@revenexx/integrations-node-sdk 0.18.1 → 1.1.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/cli.cjs CHANGED
@@ -74,7 +74,7 @@ function isSafeRelativePath(src) {
74
74
  return false;
75
75
  }
76
76
  const normalized = (0, import_node_path.normalize)(src);
77
- return normalized !== ".." && !normalized.startsWith(".." + import_node_path.sep);
77
+ return normalized !== ".." && !normalized.startsWith(`..${import_node_path.sep}`);
78
78
  }
79
79
 
80
80
  // src/extract.ts
@@ -192,6 +192,7 @@ async function main() {
192
192
  "The `publish` command has been removed. Node packages are no longer published from the repos themselves \u2014 registration happens through the Revenexx Console/Cockpit. For local development, use `integrations/scripts/register-nodes-core.sh`, which packs and uploads the tarball to the admin API."
193
193
  );
194
194
  process.exit(1);
195
+ break;
195
196
  default:
196
197
  console.error("Usage: rvnxx-nodes <manifest>");
197
198
  process.exit(1);
package/dist/cli.js CHANGED
@@ -55,7 +55,7 @@ function isSafeRelativePath(src) {
55
55
  return false;
56
56
  }
57
57
  const normalized = normalize(src);
58
- return normalized !== ".." && !normalized.startsWith(".." + sep);
58
+ return normalized !== ".." && !normalized.startsWith(`..${sep}`);
59
59
  }
60
60
 
61
61
  // src/cli.ts
@@ -132,6 +132,7 @@ async function main() {
132
132
  "The `publish` command has been removed. Node packages are no longer published from the repos themselves \u2014 registration happens through the Revenexx Console/Cockpit. For local development, use `integrations/scripts/register-nodes-core.sh`, which packs and uploads the tarball to the admin API."
133
133
  );
134
134
  process.exit(1);
135
+ break;
135
136
  default:
136
137
  console.error("Usage: rvnxx-nodes <manifest>");
137
138
  process.exit(1);
package/dist/index.cjs CHANGED
@@ -46,12 +46,15 @@ __export(index_exports, {
46
46
  NodeError: () => NodeError,
47
47
  OAuth2AuthCodeCredential: () => OAuth2AuthCodeCredential,
48
48
  OAuth2ClientCredentialsCredential: () => OAuth2ClientCredentialsCredential,
49
+ OPERATORS: () => OPERATORS,
49
50
  RetryableError: () => RetryableError,
50
51
  SimpleValueCredential: () => SimpleValueCredential,
52
+ VALUELESS_OPERATORS: () => VALUELESS_OPERATORS,
51
53
  assertPublicUrl: () => assertPublicUrl,
52
54
  backoffDelay: () => backoffDelay,
53
55
  buildManifest: () => buildManifest,
54
56
  clampResponseBytes: () => clampResponseBytes,
57
+ evaluate: () => evaluate,
55
58
  extractCredentialManifest: () => extractCredentialManifest,
56
59
  extractCredentialManifests: () => extractCredentialManifests,
57
60
  extractManifest: () => extractManifest,
@@ -59,6 +62,7 @@ __export(index_exports, {
59
62
  isBlockedAddress: () => isBlockedAddress,
60
63
  isNodeWithIteration: () => isNodeWithIteration,
61
64
  isOAuthAuthorizeCredential: () => isOAuthAuthorizeCredential,
65
+ isOperator: () => isOperator,
62
66
  maxBytesConfigField: () => maxBytesConfigField,
63
67
  normalizeCredentialType: () => normalizeCredentialType,
64
68
  normalizeLocalized: () => normalizeLocalized,
@@ -68,7 +72,9 @@ __export(index_exports, {
68
72
  readText: () => readText,
69
73
  retryConfigFields: () => retryConfigFields,
70
74
  safeFetch: () => safeFetch,
75
+ settingApplies: () => settingApplies,
71
76
  sleepWithSignal: () => sleepWithSignal,
77
+ takesValue: () => takesValue,
72
78
  timeoutConfigField: () => timeoutConfigField,
73
79
  withRetry: () => withRetry
74
80
  });
@@ -103,6 +109,75 @@ function normalizeLocalized(value, fallbackLang = "en") {
103
109
  return void 0;
104
110
  }
105
111
 
112
+ // src/operators.ts
113
+ var OPERATORS = [
114
+ "equals",
115
+ "notEquals",
116
+ "contains",
117
+ "notContains",
118
+ "startsWith",
119
+ "endsWith",
120
+ "greaterThan",
121
+ "greaterThanOrEqual",
122
+ "lessThan",
123
+ "lessThanOrEqual",
124
+ "exists",
125
+ "notExists",
126
+ "isEmpty",
127
+ "isNotEmpty"
128
+ ];
129
+ var VALUELESS_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
130
+ function isOperator(value) {
131
+ return typeof value === "string" && OPERATORS.includes(value);
132
+ }
133
+ function takesValue(op) {
134
+ return !VALUELESS_OPERATORS.includes(op);
135
+ }
136
+ function evaluate(left, op, right) {
137
+ switch (op) {
138
+ case "exists":
139
+ return left !== void 0 && left !== null;
140
+ case "notExists":
141
+ return left === void 0 || left === null;
142
+ case "isEmpty":
143
+ return left === "" || left === null || left === void 0 || Array.isArray(left) && left.length === 0;
144
+ case "isNotEmpty":
145
+ return !evaluate(left, "isEmpty", right);
146
+ case "equals":
147
+ return String(left) === String(right);
148
+ case "notEquals":
149
+ return String(left) !== String(right);
150
+ case "contains":
151
+ return typeof left === "string" && left.includes(String(right));
152
+ case "notContains":
153
+ return typeof left === "string" && !left.includes(String(right));
154
+ case "startsWith":
155
+ return typeof left === "string" && left.startsWith(String(right));
156
+ case "endsWith":
157
+ return typeof left === "string" && left.endsWith(String(right));
158
+ case "greaterThan":
159
+ return Number(left) > Number(right);
160
+ case "greaterThanOrEqual":
161
+ return Number(left) >= Number(right);
162
+ case "lessThan":
163
+ return Number(left) < Number(right);
164
+ case "lessThanOrEqual":
165
+ return Number(left) <= Number(right);
166
+ default: {
167
+ const _exhaustive = op;
168
+ void _exhaustive;
169
+ return false;
170
+ }
171
+ }
172
+ }
173
+
174
+ // src/conditions.ts
175
+ function settingApplies(field, config) {
176
+ const condition = field.showIf;
177
+ if (!condition) return true;
178
+ return evaluate(config[condition.key], condition.op, condition.value);
179
+ }
180
+
106
181
  // src/credentialType.ts
107
182
  function normalizeCredentialType(value) {
108
183
  const raw = value === void 0 ? [] : Array.isArray(value) ? value : [value];
@@ -526,7 +601,7 @@ function abortReason(signal) {
526
601
  }
527
602
  function backoffDelay(attempt, policy) {
528
603
  const exponent = Math.max(0, attempt - 1);
529
- const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
604
+ const raw = policy.baseDelayMs * policy.factor ** exponent;
530
605
  const cap = Math.min(policy.maxDelayMs, raw);
531
606
  return policy.jitter ? Math.random() * cap : cap;
532
607
  }
@@ -628,7 +703,25 @@ var BaseCredential = class {
628
703
  }
629
704
  return { ok: true };
630
705
  }
631
- /** POST `application/x-www-form-urlencoded` and parse a JSON token response. */
706
+ /**
707
+ * POST `application/x-www-form-urlencoded` and parse a JSON token response.
708
+ *
709
+ * Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
710
+ * endpoint is not a constant: `tokenUrl(config)` derives it from credential
711
+ * config, i.e. from a value someone types into a credential form. A raw fetch
712
+ * would happily POST the client secret at `http://169.254.169.254/…` or any
713
+ * other internal address, which is precisely what `assertPublicUrl` exists to
714
+ * prevent. Every OAuth token exchange in this file — client-credentials,
715
+ * auth-code, refresh — funnels through here, so this is the one call site that
716
+ * has to hold.
717
+ *
718
+ * Two behaviours safeFetch adds, both wanted here: a request budget
719
+ * (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
720
+ * that re-checks each hop and drops `Authorization` across an origin boundary.
721
+ * Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
722
+ * per the redirect rules, not a quirk of ours. Real token endpoints do not
723
+ * redirect; one that does was never going to complete the exchange anyway.
724
+ */
632
725
  async postForm(ctx, url, form, headers = {}) {
633
726
  const body = new URLSearchParams();
634
727
  for (const [key, value] of Object.entries(form)) {
@@ -636,7 +729,7 @@ var BaseCredential = class {
636
729
  body.set(key, value);
637
730
  }
638
731
  }
639
- const res = await fetch(url, {
732
+ const res = await safeFetch(url, {
640
733
  method: "POST",
641
734
  signal: ctx.signal,
642
735
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", ...headers },
@@ -821,12 +914,15 @@ function base64Url(buf) {
821
914
  NodeError,
822
915
  OAuth2AuthCodeCredential,
823
916
  OAuth2ClientCredentialsCredential,
917
+ OPERATORS,
824
918
  RetryableError,
825
919
  SimpleValueCredential,
920
+ VALUELESS_OPERATORS,
826
921
  assertPublicUrl,
827
922
  backoffDelay,
828
923
  buildManifest,
829
924
  clampResponseBytes,
925
+ evaluate,
830
926
  extractCredentialManifest,
831
927
  extractCredentialManifests,
832
928
  extractManifest,
@@ -834,6 +930,7 @@ function base64Url(buf) {
834
930
  isBlockedAddress,
835
931
  isNodeWithIteration,
836
932
  isOAuthAuthorizeCredential,
933
+ isOperator,
837
934
  maxBytesConfigField,
838
935
  normalizeCredentialType,
839
936
  normalizeLocalized,
@@ -843,7 +940,9 @@ function base64Url(buf) {
843
940
  readText,
844
941
  retryConfigFields,
845
942
  safeFetch,
943
+ settingApplies,
846
944
  sleepWithSignal,
945
+ takesValue,
847
946
  timeoutConfigField,
848
947
  withRetry
849
948
  });
package/dist/index.d.cts CHANGED
@@ -1,8 +1,36 @@
1
+ /**
2
+ * The comparison vocabulary, in one place because it is read in three:
3
+ * a node that compares two values (a condition, a filter), a settings condition
4
+ * saying when a field applies (`IConfigField.showIf`), and the editor drawing
5
+ * that field. An author who has met these words in one of the three must not
6
+ * have to learn a second set for the others.
7
+ *
8
+ * The names are the vocabulary; what each one *means* is {@link evaluate}, and
9
+ * the answer table in `operators.test.ts` is what a second implementation of it
10
+ * — the editor, the platform's workflow validator — is checked against.
11
+ */
12
+ declare const OPERATORS: readonly ["equals", "notEquals", "contains", "notContains", "startsWith", "endsWith", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "exists", "notExists", "isEmpty", "isNotEmpty"];
13
+ type Operator = (typeof OPERATORS)[number];
14
+ /** The four that read whether a value is there rather than what it is. */
15
+ declare const VALUELESS_OPERATORS: readonly Operator[];
16
+ declare function isOperator(value: unknown): value is Operator;
17
+ /** Whether an operator reads a second value at all, or only the first. */
18
+ declare function takesValue(op: Operator): boolean;
19
+ /**
20
+ * What an operator means.
21
+ *
22
+ * Equality is deliberately lenient across the string/number line: a `select`
23
+ * carries `1` where a text box carries `'1'`, and an author means one thing by
24
+ * both. A second implementation has to reproduce that — and has to be careful
25
+ * with booleans, whose default stringification differs between languages.
26
+ */
27
+ declare function evaluate(left: unknown, op: Operator, right: unknown): boolean;
28
+
1
29
  type LocalizedString = string | Record<string, string>;
2
30
  type DataType = 'any' | 'object' | 'array' | 'string' | 'number' | 'boolean';
3
31
  type OutputKind = 'default' | 'branch' | 'error';
4
32
  type NodeCategory = 'trigger' | 'action' | 'transform' | 'control' | 'io';
5
- type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'dynamic-schema';
33
+ type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'state-ref' | 'dynamic-schema';
6
34
  /**
7
35
  * Semantic category of an image, driving how the registry/CDN organises and
8
36
  * displays it.
@@ -63,6 +91,26 @@ interface IConfigValidation {
63
91
  minLength?: number;
64
92
  maxLength?: number;
65
93
  }
94
+ /**
95
+ * A condition on another setting's value, deciding whether this one applies.
96
+ *
97
+ * One condition, one key, one operator — `showIf: { key: 'source', op:
98
+ * 'equals', value: 'field' }` reads as the sentence a node would otherwise
99
+ * write under the field. The driving key MUST be a literal (it may not set
100
+ * `expressionAllowed`), because applicability has to be decidable while the
101
+ * author is typing; the same limit `dependsOn` carries, for the same reason.
102
+ */
103
+ interface IShowIfCondition {
104
+ /** The `key` of another config field on the same node. */
105
+ key: string;
106
+ /** How to compare it — the shared comparison vocabulary, see `OPERATORS`. */
107
+ op: Operator;
108
+ /**
109
+ * What to compare it to. Left out by the four operators that read presence
110
+ * rather than content (`exists`, `notExists`, `isEmpty`, `isNotEmpty`).
111
+ */
112
+ value?: string | number | boolean;
113
+ }
66
114
  interface IConfigFieldBase {
67
115
  key: string;
68
116
  label: LocalizedString;
@@ -92,6 +140,20 @@ interface IConfigFieldBase {
92
140
  * `expressionAllowed`), so its value is known at author time.
93
141
  */
94
142
  dependsOn?: string[];
143
+ /**
144
+ * When this field applies at all. The editor draws it only while the
145
+ * condition holds, so a node says once in its manifest what it would
146
+ * otherwise have to explain in prose under every affected field.
147
+ *
148
+ * Not to be confused with `dependsOn`, which sits beside it and does a
149
+ * different thing: `dependsOn` re-*resolves* a dynamic field's options when
150
+ * another field changes, and never decides whether the field is drawn.
151
+ *
152
+ * Left unset, the field always applies — so this is additive: a node that
153
+ * says nothing behaves as it always did, and so does an editor that does not
154
+ * know the key.
155
+ */
156
+ showIf?: IShowIfCondition;
95
157
  /**
96
158
  * Only meaningful when `type === 'credentials-ref'`: the namespaced slug(s) of
97
159
  * the credential type(s) this field accepts (e.g. `revenexx:smtp`). The editor
@@ -101,10 +163,43 @@ interface IConfigFieldBase {
101
163
  * a node that works with both an OAuth and an API-token credential).
102
164
  */
103
165
  credentialType?: string | string[];
166
+ /**
167
+ * Only meaningful when `type === 'state-ref'`: which kind of state namespace
168
+ * this field accepts (PO-374). The editor lists the workflow's declared
169
+ * namespaces of that role — and lets the author declare a new one without
170
+ * leaving the node — and the blob stores the chosen NAME.
171
+ *
172
+ * Declare the field rather than hardcoding a namespace name in `execute`: a
173
+ * literal is a contract nothing checks, and the mismatch surfaces as a 403 in
174
+ * the first production run rather than as an empty picker while authoring. It
175
+ * also narrows what the node may reach — a namespace is reachable only from
176
+ * the node whose config names it.
177
+ */
178
+ stateRole?: 'mapping' | 'cursor' | 'dedupe' | 'digest';
104
179
  }
105
180
  interface IConfigField extends IConfigFieldBase {
106
- properties?: IConfigFieldBase[];
107
- items?: IConfigFieldBase;
181
+ /**
182
+ * The sub-settings of a field that holds a group of them (`type: 'object'`).
183
+ *
184
+ * A full `IConfigField`, not the base. `IConfigFieldBase` already carries
185
+ * `options`, `default`, `required`, `description` and `showIf`, so those were
186
+ * never what a nested setting was missing — `properties` and `items` were: the
187
+ * base has neither, so a sub-setting could not itself group or repeat without
188
+ * a cast. The manifest schema permits that nesting and the engine's config
189
+ * walker reads nested fields as first-class; the type was the only place
190
+ * stopping at one level (PO-436).
191
+ */
192
+ properties?: IConfigField[];
193
+ /**
194
+ * What one entry of a list field (`type: 'array'`) looks like.
195
+ *
196
+ * Full `IConfigField` for the same reason as `properties`, and this is the one
197
+ * that bit: `items.type: 'object'` plus `items.properties` is the repeating row
198
+ * every mapping-style setting is built from, and `items` typed as the base had
199
+ * no `properties` to give it. `SwitchNode` in integrations-nodes-core ends its
200
+ * items block with `as IConfigField` for exactly that reason.
201
+ */
202
+ items?: IConfigField;
108
203
  }
109
204
  interface INodeDescription {
110
205
  slug: string;
@@ -153,6 +248,101 @@ interface INodeContext {
153
248
  credentials: {
154
249
  get(credentialsId: string): Promise<Record<string, unknown>>;
155
250
  };
251
+ /**
252
+ * The tenant state store: what this workflow already knows from earlier runs
253
+ * (PO-374). Every call names a namespace the workflow declared in its
254
+ * `state[]` block; anything else is refused by the engine, so a node cannot
255
+ * reach state its workflow did not ask for.
256
+ */
257
+ state: INodeState;
258
+ }
259
+ /**
260
+ * The four things a workflow can remember between runs, and the reason each is
261
+ * its own operation rather than a generic get/set: the role decides *when* a
262
+ * write becomes visible, and that decision is the engine's to make, not the
263
+ * node author's.
264
+ *
265
+ * The four roles are named **mapping**, **cursor**, **dedupe** and **digest** —
266
+ * those are the values a `state-ref` field's `stateRole` takes. `claim` below is
267
+ * the *operation* on a dedupe namespace, not a role of its own.
268
+ *
269
+ * - **mapping** and **dedupe** take effect immediately. A correlation discarded
270
+ * because the run failed afterwards is how the next run creates a duplicate
271
+ * in the target system; a claim invisible to other runs protects against
272
+ * nothing.
273
+ * - **cursor** and **digest** are staged and adopted only when the run
274
+ * completes. A watermark that advances on a failed run leaves exactly the gap
275
+ * it exists to prevent, and a digest may only count once the write it
276
+ * describes actually went through.
277
+ *
278
+ * At author time — the editor's "test this node" button — the store is
279
+ * **read-only**: a rehearsal must not leave correlations behind that a later
280
+ * production run would treat as truth. The write calls reject there.
281
+ */
282
+ interface INodeState {
283
+ /** Correlate an id on each side of an integration (PIM article ↔ ERP number). */
284
+ mapping: {
285
+ /**
286
+ * The partner id of a correlation, or `null` when it is unknown — which is
287
+ * the usual way to decide between creating and updating in the target
288
+ * system.
289
+ *
290
+ * `side` says which side of the pair `key` is on, not which side to return:
291
+ * the default `'left'` matches `key` against the left id and answers with
292
+ * the right one, and `'right'` does the reverse. It never searches both, so
293
+ * a lookup on the wrong side answers `null` rather than falling back — and
294
+ * `null` on the create path is what makes the target record a second time.
295
+ */
296
+ get(namespace: string, key: string, side?: 'left' | 'right'): Promise<string | null>;
297
+ /**
298
+ * Record a correlation. Takes effect immediately, and re-pointing one side
299
+ * of an existing pair is refused: an id correlated two ways is always a bug,
300
+ * and no later sync can untangle it.
301
+ */
302
+ put(namespace: string, left: string, right: string, metadata?: Record<string, unknown>): Promise<void>;
303
+ };
304
+ /** How far an incremental sync has read. */
305
+ cursor: {
306
+ /**
307
+ * The committed watermark — an arbitrary JSON value such as
308
+ * `{ updatedAfter }` or a provider page token — or `undefined` when the
309
+ * sync has never run. A value staged by *this* run is not visible yet.
310
+ */
311
+ get(namespace: string, partitionKey?: string): Promise<unknown>;
312
+ /**
313
+ * Stage a watermark. Adopted only if the run completes, so a failed run
314
+ * leaves the previous value in place and the next run picks the gap up.
315
+ * `partitionKey` separates e.g. one shop or company code from another.
316
+ */
317
+ set(namespace: string, value: unknown, partitionKey?: string): Promise<void>;
318
+ };
319
+ /**
320
+ * Claim a key for the duration of its TTL. `true` means this caller got it;
321
+ * `false` means the key is held and this delivery is a duplicate — the normal
322
+ * answer under at-least-once delivery, and a value to branch on rather than an
323
+ * error.
324
+ *
325
+ * **A claim outlives the attempt that made it.** The holder is not identified:
326
+ * a claim is taken over only once it has expired, so a retry of the very
327
+ * attempt that claimed the key — after a transient failure that happened
328
+ * *after* the claim — is also told `false`. Treating that as a duplicate drops
329
+ * the delivery for the rest of the TTL, which makes `ttlSeconds` the retry
330
+ * window as much as the duplicate-suppression window. Choose it against both,
331
+ * and do not claim earlier in the node than necessary.
332
+ *
333
+ * `ttlSeconds` defaults to 604800 (seven days) and is accepted between 1 and
334
+ * 31536000.
335
+ */
336
+ claim(namespace: string, key: string, opts?: {
337
+ ttlSeconds?: number;
338
+ }): Promise<boolean>;
339
+ /** Skip expensive writes for entities that have not changed. */
340
+ digest: {
341
+ /** Whether the entity still carries this hash — i.e. the write can be skipped. */
342
+ unchanged(namespace: string, entityKey: string, digest: string): Promise<boolean>;
343
+ /** Stage a hash; adopted only if the run completes. Call it *after* the write. */
344
+ set(namespace: string, entityKey: string, digest: string): Promise<void>;
345
+ };
156
346
  }
157
347
  interface INodeResult {
158
348
  outputs: Record<string, unknown>;
@@ -420,6 +610,22 @@ interface ITemplateDescription {
420
610
  */
421
611
  declare function normalizeLocalized(value: LocalizedString | undefined | null, fallbackLang?: string): string | undefined;
422
612
 
613
+ /**
614
+ * Whether a setting applies, given what the author has filled in so far.
615
+ *
616
+ * A setting with no condition always applies — which is what makes `showIf`
617
+ * additive: a node that says nothing gets the behaviour it has today, and an
618
+ * editor that does not know the key draws every field as before.
619
+ *
620
+ * The value the condition reads is taken from the config as it stands mid-edit,
621
+ * so it is routinely `undefined`: the moment a node is dropped on the canvas
622
+ * nothing is filled in. That is why `equals` against a missing value is `false`
623
+ * rather than an error — the setting does not apply *yet*.
624
+ */
625
+ declare function settingApplies(field: {
626
+ showIf?: IShowIfCondition;
627
+ }, config: Record<string, unknown>): boolean;
628
+
423
629
  /**
424
630
  * Reduce an `IConfigField.credentialType` (a single slug or an array of slugs)
425
631
  * to a normalized, deduplicated `string[]`.
@@ -728,7 +934,25 @@ declare abstract class BaseCredential implements ICredential {
728
934
  abstract readonly description: ICredentialDescription;
729
935
  abstract resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
730
936
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
731
- /** POST `application/x-www-form-urlencoded` and parse a JSON token response. */
937
+ /**
938
+ * POST `application/x-www-form-urlencoded` and parse a JSON token response.
939
+ *
940
+ * Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
941
+ * endpoint is not a constant: `tokenUrl(config)` derives it from credential
942
+ * config, i.e. from a value someone types into a credential form. A raw fetch
943
+ * would happily POST the client secret at `http://169.254.169.254/…` or any
944
+ * other internal address, which is precisely what `assertPublicUrl` exists to
945
+ * prevent. Every OAuth token exchange in this file — client-credentials,
946
+ * auth-code, refresh — funnels through here, so this is the one call site that
947
+ * has to hold.
948
+ *
949
+ * Two behaviours safeFetch adds, both wanted here: a request budget
950
+ * (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
951
+ * that re-checks each hop and drops `Authorization` across an origin boundary.
952
+ * Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
953
+ * per the redirect rules, not a quirk of ours. Real token endpoints do not
954
+ * redirect; one that does was never going to complete the exchange anyway.
955
+ */
732
956
  protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
733
957
  }
734
958
  /**
@@ -804,4 +1028,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
804
1028
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
805
1029
  }
806
1030
 
807
- 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, 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, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
1031
+ 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 };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,36 @@
1
+ /**
2
+ * The comparison vocabulary, in one place because it is read in three:
3
+ * a node that compares two values (a condition, a filter), a settings condition
4
+ * saying when a field applies (`IConfigField.showIf`), and the editor drawing
5
+ * that field. An author who has met these words in one of the three must not
6
+ * have to learn a second set for the others.
7
+ *
8
+ * The names are the vocabulary; what each one *means* is {@link evaluate}, and
9
+ * the answer table in `operators.test.ts` is what a second implementation of it
10
+ * — the editor, the platform's workflow validator — is checked against.
11
+ */
12
+ declare const OPERATORS: readonly ["equals", "notEquals", "contains", "notContains", "startsWith", "endsWith", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "exists", "notExists", "isEmpty", "isNotEmpty"];
13
+ type Operator = (typeof OPERATORS)[number];
14
+ /** The four that read whether a value is there rather than what it is. */
15
+ declare const VALUELESS_OPERATORS: readonly Operator[];
16
+ declare function isOperator(value: unknown): value is Operator;
17
+ /** Whether an operator reads a second value at all, or only the first. */
18
+ declare function takesValue(op: Operator): boolean;
19
+ /**
20
+ * What an operator means.
21
+ *
22
+ * Equality is deliberately lenient across the string/number line: a `select`
23
+ * carries `1` where a text box carries `'1'`, and an author means one thing by
24
+ * both. A second implementation has to reproduce that — and has to be careful
25
+ * with booleans, whose default stringification differs between languages.
26
+ */
27
+ declare function evaluate(left: unknown, op: Operator, right: unknown): boolean;
28
+
1
29
  type LocalizedString = string | Record<string, string>;
2
30
  type DataType = 'any' | 'object' | 'array' | 'string' | 'number' | 'boolean';
3
31
  type OutputKind = 'default' | 'branch' | 'error';
4
32
  type NodeCategory = 'trigger' | 'action' | 'transform' | 'control' | 'io';
5
- type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'dynamic-schema';
33
+ type ConfigType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'state-ref' | 'dynamic-schema';
6
34
  /**
7
35
  * Semantic category of an image, driving how the registry/CDN organises and
8
36
  * displays it.
@@ -63,6 +91,26 @@ interface IConfigValidation {
63
91
  minLength?: number;
64
92
  maxLength?: number;
65
93
  }
94
+ /**
95
+ * A condition on another setting's value, deciding whether this one applies.
96
+ *
97
+ * One condition, one key, one operator — `showIf: { key: 'source', op:
98
+ * 'equals', value: 'field' }` reads as the sentence a node would otherwise
99
+ * write under the field. The driving key MUST be a literal (it may not set
100
+ * `expressionAllowed`), because applicability has to be decidable while the
101
+ * author is typing; the same limit `dependsOn` carries, for the same reason.
102
+ */
103
+ interface IShowIfCondition {
104
+ /** The `key` of another config field on the same node. */
105
+ key: string;
106
+ /** How to compare it — the shared comparison vocabulary, see `OPERATORS`. */
107
+ op: Operator;
108
+ /**
109
+ * What to compare it to. Left out by the four operators that read presence
110
+ * rather than content (`exists`, `notExists`, `isEmpty`, `isNotEmpty`).
111
+ */
112
+ value?: string | number | boolean;
113
+ }
66
114
  interface IConfigFieldBase {
67
115
  key: string;
68
116
  label: LocalizedString;
@@ -92,6 +140,20 @@ interface IConfigFieldBase {
92
140
  * `expressionAllowed`), so its value is known at author time.
93
141
  */
94
142
  dependsOn?: string[];
143
+ /**
144
+ * When this field applies at all. The editor draws it only while the
145
+ * condition holds, so a node says once in its manifest what it would
146
+ * otherwise have to explain in prose under every affected field.
147
+ *
148
+ * Not to be confused with `dependsOn`, which sits beside it and does a
149
+ * different thing: `dependsOn` re-*resolves* a dynamic field's options when
150
+ * another field changes, and never decides whether the field is drawn.
151
+ *
152
+ * Left unset, the field always applies — so this is additive: a node that
153
+ * says nothing behaves as it always did, and so does an editor that does not
154
+ * know the key.
155
+ */
156
+ showIf?: IShowIfCondition;
95
157
  /**
96
158
  * Only meaningful when `type === 'credentials-ref'`: the namespaced slug(s) of
97
159
  * the credential type(s) this field accepts (e.g. `revenexx:smtp`). The editor
@@ -101,10 +163,43 @@ interface IConfigFieldBase {
101
163
  * a node that works with both an OAuth and an API-token credential).
102
164
  */
103
165
  credentialType?: string | string[];
166
+ /**
167
+ * Only meaningful when `type === 'state-ref'`: which kind of state namespace
168
+ * this field accepts (PO-374). The editor lists the workflow's declared
169
+ * namespaces of that role — and lets the author declare a new one without
170
+ * leaving the node — and the blob stores the chosen NAME.
171
+ *
172
+ * Declare the field rather than hardcoding a namespace name in `execute`: a
173
+ * literal is a contract nothing checks, and the mismatch surfaces as a 403 in
174
+ * the first production run rather than as an empty picker while authoring. It
175
+ * also narrows what the node may reach — a namespace is reachable only from
176
+ * the node whose config names it.
177
+ */
178
+ stateRole?: 'mapping' | 'cursor' | 'dedupe' | 'digest';
104
179
  }
105
180
  interface IConfigField extends IConfigFieldBase {
106
- properties?: IConfigFieldBase[];
107
- items?: IConfigFieldBase;
181
+ /**
182
+ * The sub-settings of a field that holds a group of them (`type: 'object'`).
183
+ *
184
+ * A full `IConfigField`, not the base. `IConfigFieldBase` already carries
185
+ * `options`, `default`, `required`, `description` and `showIf`, so those were
186
+ * never what a nested setting was missing — `properties` and `items` were: the
187
+ * base has neither, so a sub-setting could not itself group or repeat without
188
+ * a cast. The manifest schema permits that nesting and the engine's config
189
+ * walker reads nested fields as first-class; the type was the only place
190
+ * stopping at one level (PO-436).
191
+ */
192
+ properties?: IConfigField[];
193
+ /**
194
+ * What one entry of a list field (`type: 'array'`) looks like.
195
+ *
196
+ * Full `IConfigField` for the same reason as `properties`, and this is the one
197
+ * that bit: `items.type: 'object'` plus `items.properties` is the repeating row
198
+ * every mapping-style setting is built from, and `items` typed as the base had
199
+ * no `properties` to give it. `SwitchNode` in integrations-nodes-core ends its
200
+ * items block with `as IConfigField` for exactly that reason.
201
+ */
202
+ items?: IConfigField;
108
203
  }
109
204
  interface INodeDescription {
110
205
  slug: string;
@@ -153,6 +248,101 @@ interface INodeContext {
153
248
  credentials: {
154
249
  get(credentialsId: string): Promise<Record<string, unknown>>;
155
250
  };
251
+ /**
252
+ * The tenant state store: what this workflow already knows from earlier runs
253
+ * (PO-374). Every call names a namespace the workflow declared in its
254
+ * `state[]` block; anything else is refused by the engine, so a node cannot
255
+ * reach state its workflow did not ask for.
256
+ */
257
+ state: INodeState;
258
+ }
259
+ /**
260
+ * The four things a workflow can remember between runs, and the reason each is
261
+ * its own operation rather than a generic get/set: the role decides *when* a
262
+ * write becomes visible, and that decision is the engine's to make, not the
263
+ * node author's.
264
+ *
265
+ * The four roles are named **mapping**, **cursor**, **dedupe** and **digest** —
266
+ * those are the values a `state-ref` field's `stateRole` takes. `claim` below is
267
+ * the *operation* on a dedupe namespace, not a role of its own.
268
+ *
269
+ * - **mapping** and **dedupe** take effect immediately. A correlation discarded
270
+ * because the run failed afterwards is how the next run creates a duplicate
271
+ * in the target system; a claim invisible to other runs protects against
272
+ * nothing.
273
+ * - **cursor** and **digest** are staged and adopted only when the run
274
+ * completes. A watermark that advances on a failed run leaves exactly the gap
275
+ * it exists to prevent, and a digest may only count once the write it
276
+ * describes actually went through.
277
+ *
278
+ * At author time — the editor's "test this node" button — the store is
279
+ * **read-only**: a rehearsal must not leave correlations behind that a later
280
+ * production run would treat as truth. The write calls reject there.
281
+ */
282
+ interface INodeState {
283
+ /** Correlate an id on each side of an integration (PIM article ↔ ERP number). */
284
+ mapping: {
285
+ /**
286
+ * The partner id of a correlation, or `null` when it is unknown — which is
287
+ * the usual way to decide between creating and updating in the target
288
+ * system.
289
+ *
290
+ * `side` says which side of the pair `key` is on, not which side to return:
291
+ * the default `'left'` matches `key` against the left id and answers with
292
+ * the right one, and `'right'` does the reverse. It never searches both, so
293
+ * a lookup on the wrong side answers `null` rather than falling back — and
294
+ * `null` on the create path is what makes the target record a second time.
295
+ */
296
+ get(namespace: string, key: string, side?: 'left' | 'right'): Promise<string | null>;
297
+ /**
298
+ * Record a correlation. Takes effect immediately, and re-pointing one side
299
+ * of an existing pair is refused: an id correlated two ways is always a bug,
300
+ * and no later sync can untangle it.
301
+ */
302
+ put(namespace: string, left: string, right: string, metadata?: Record<string, unknown>): Promise<void>;
303
+ };
304
+ /** How far an incremental sync has read. */
305
+ cursor: {
306
+ /**
307
+ * The committed watermark — an arbitrary JSON value such as
308
+ * `{ updatedAfter }` or a provider page token — or `undefined` when the
309
+ * sync has never run. A value staged by *this* run is not visible yet.
310
+ */
311
+ get(namespace: string, partitionKey?: string): Promise<unknown>;
312
+ /**
313
+ * Stage a watermark. Adopted only if the run completes, so a failed run
314
+ * leaves the previous value in place and the next run picks the gap up.
315
+ * `partitionKey` separates e.g. one shop or company code from another.
316
+ */
317
+ set(namespace: string, value: unknown, partitionKey?: string): Promise<void>;
318
+ };
319
+ /**
320
+ * Claim a key for the duration of its TTL. `true` means this caller got it;
321
+ * `false` means the key is held and this delivery is a duplicate — the normal
322
+ * answer under at-least-once delivery, and a value to branch on rather than an
323
+ * error.
324
+ *
325
+ * **A claim outlives the attempt that made it.** The holder is not identified:
326
+ * a claim is taken over only once it has expired, so a retry of the very
327
+ * attempt that claimed the key — after a transient failure that happened
328
+ * *after* the claim — is also told `false`. Treating that as a duplicate drops
329
+ * the delivery for the rest of the TTL, which makes `ttlSeconds` the retry
330
+ * window as much as the duplicate-suppression window. Choose it against both,
331
+ * and do not claim earlier in the node than necessary.
332
+ *
333
+ * `ttlSeconds` defaults to 604800 (seven days) and is accepted between 1 and
334
+ * 31536000.
335
+ */
336
+ claim(namespace: string, key: string, opts?: {
337
+ ttlSeconds?: number;
338
+ }): Promise<boolean>;
339
+ /** Skip expensive writes for entities that have not changed. */
340
+ digest: {
341
+ /** Whether the entity still carries this hash — i.e. the write can be skipped. */
342
+ unchanged(namespace: string, entityKey: string, digest: string): Promise<boolean>;
343
+ /** Stage a hash; adopted only if the run completes. Call it *after* the write. */
344
+ set(namespace: string, entityKey: string, digest: string): Promise<void>;
345
+ };
156
346
  }
157
347
  interface INodeResult {
158
348
  outputs: Record<string, unknown>;
@@ -420,6 +610,22 @@ interface ITemplateDescription {
420
610
  */
421
611
  declare function normalizeLocalized(value: LocalizedString | undefined | null, fallbackLang?: string): string | undefined;
422
612
 
613
+ /**
614
+ * Whether a setting applies, given what the author has filled in so far.
615
+ *
616
+ * A setting with no condition always applies — which is what makes `showIf`
617
+ * additive: a node that says nothing gets the behaviour it has today, and an
618
+ * editor that does not know the key draws every field as before.
619
+ *
620
+ * The value the condition reads is taken from the config as it stands mid-edit,
621
+ * so it is routinely `undefined`: the moment a node is dropped on the canvas
622
+ * nothing is filled in. That is why `equals` against a missing value is `false`
623
+ * rather than an error — the setting does not apply *yet*.
624
+ */
625
+ declare function settingApplies(field: {
626
+ showIf?: IShowIfCondition;
627
+ }, config: Record<string, unknown>): boolean;
628
+
423
629
  /**
424
630
  * Reduce an `IConfigField.credentialType` (a single slug or an array of slugs)
425
631
  * to a normalized, deduplicated `string[]`.
@@ -728,7 +934,25 @@ declare abstract class BaseCredential implements ICredential {
728
934
  abstract readonly description: ICredentialDescription;
729
935
  abstract resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
730
936
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
731
- /** POST `application/x-www-form-urlencoded` and parse a JSON token response. */
937
+ /**
938
+ * POST `application/x-www-form-urlencoded` and parse a JSON token response.
939
+ *
940
+ * Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
941
+ * endpoint is not a constant: `tokenUrl(config)` derives it from credential
942
+ * config, i.e. from a value someone types into a credential form. A raw fetch
943
+ * would happily POST the client secret at `http://169.254.169.254/…` or any
944
+ * other internal address, which is precisely what `assertPublicUrl` exists to
945
+ * prevent. Every OAuth token exchange in this file — client-credentials,
946
+ * auth-code, refresh — funnels through here, so this is the one call site that
947
+ * has to hold.
948
+ *
949
+ * Two behaviours safeFetch adds, both wanted here: a request budget
950
+ * (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
951
+ * that re-checks each hop and drops `Authorization` across an origin boundary.
952
+ * Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
953
+ * per the redirect rules, not a quirk of ours. Real token endpoints do not
954
+ * redirect; one that does was never going to complete the exchange anyway.
955
+ */
732
956
  protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
733
957
  }
734
958
  /**
@@ -804,4 +1028,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
804
1028
  test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
805
1029
  }
806
1030
 
807
- 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, 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, type OutputKind, type RetryHooks, type RetryPolicy, RetryableError, type SafeFetchOptions, type SafeFetchRetry, SimpleValueCredential, type TemplateLevel, type TemplateTriggerType, assertPublicUrl, backoffDelay, buildManifest, clampResponseBytes, extractCredentialManifest, extractCredentialManifests, extractManifest, extractManifests, isBlockedAddress, isNodeWithIteration, isOAuthAuthorizeCredential, maxBytesConfigField, normalizeCredentialType, normalizeLocalized, parsePackageMeta, readArrayBuffer, readJsonOrText, readText, retryConfigFields, safeFetch, sleepWithSignal, timeoutConfigField, withRetry };
1031
+ 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 };
package/dist/index.js CHANGED
@@ -37,6 +37,75 @@ function normalizeLocalized(value, fallbackLang = "en") {
37
37
  return void 0;
38
38
  }
39
39
 
40
+ // src/operators.ts
41
+ var OPERATORS = [
42
+ "equals",
43
+ "notEquals",
44
+ "contains",
45
+ "notContains",
46
+ "startsWith",
47
+ "endsWith",
48
+ "greaterThan",
49
+ "greaterThanOrEqual",
50
+ "lessThan",
51
+ "lessThanOrEqual",
52
+ "exists",
53
+ "notExists",
54
+ "isEmpty",
55
+ "isNotEmpty"
56
+ ];
57
+ var VALUELESS_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
58
+ function isOperator(value) {
59
+ return typeof value === "string" && OPERATORS.includes(value);
60
+ }
61
+ function takesValue(op) {
62
+ return !VALUELESS_OPERATORS.includes(op);
63
+ }
64
+ function evaluate(left, op, right) {
65
+ switch (op) {
66
+ case "exists":
67
+ return left !== void 0 && left !== null;
68
+ case "notExists":
69
+ return left === void 0 || left === null;
70
+ case "isEmpty":
71
+ return left === "" || left === null || left === void 0 || Array.isArray(left) && left.length === 0;
72
+ case "isNotEmpty":
73
+ return !evaluate(left, "isEmpty", right);
74
+ case "equals":
75
+ return String(left) === String(right);
76
+ case "notEquals":
77
+ return String(left) !== String(right);
78
+ case "contains":
79
+ return typeof left === "string" && left.includes(String(right));
80
+ case "notContains":
81
+ return typeof left === "string" && !left.includes(String(right));
82
+ case "startsWith":
83
+ return typeof left === "string" && left.startsWith(String(right));
84
+ case "endsWith":
85
+ return typeof left === "string" && left.endsWith(String(right));
86
+ case "greaterThan":
87
+ return Number(left) > Number(right);
88
+ case "greaterThanOrEqual":
89
+ return Number(left) >= Number(right);
90
+ case "lessThan":
91
+ return Number(left) < Number(right);
92
+ case "lessThanOrEqual":
93
+ return Number(left) <= Number(right);
94
+ default: {
95
+ const _exhaustive = op;
96
+ void _exhaustive;
97
+ return false;
98
+ }
99
+ }
100
+ }
101
+
102
+ // src/conditions.ts
103
+ function settingApplies(field, config) {
104
+ const condition = field.showIf;
105
+ if (!condition) return true;
106
+ return evaluate(config[condition.key], condition.op, condition.value);
107
+ }
108
+
40
109
  // src/credentialType.ts
41
110
  function normalizeCredentialType(value) {
42
111
  const raw = value === void 0 ? [] : Array.isArray(value) ? value : [value];
@@ -446,7 +515,7 @@ function abortReason(signal) {
446
515
  }
447
516
  function backoffDelay(attempt, policy) {
448
517
  const exponent = Math.max(0, attempt - 1);
449
- const raw = policy.baseDelayMs * Math.pow(policy.factor, exponent);
518
+ const raw = policy.baseDelayMs * policy.factor ** exponent;
450
519
  const cap = Math.min(policy.maxDelayMs, raw);
451
520
  return policy.jitter ? Math.random() * cap : cap;
452
521
  }
@@ -521,7 +590,25 @@ var BaseCredential = class {
521
590
  }
522
591
  return { ok: true };
523
592
  }
524
- /** POST `application/x-www-form-urlencoded` and parse a JSON token response. */
593
+ /**
594
+ * POST `application/x-www-form-urlencoded` and parse a JSON token response.
595
+ *
596
+ * Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
597
+ * endpoint is not a constant: `tokenUrl(config)` derives it from credential
598
+ * config, i.e. from a value someone types into a credential form. A raw fetch
599
+ * would happily POST the client secret at `http://169.254.169.254/…` or any
600
+ * other internal address, which is precisely what `assertPublicUrl` exists to
601
+ * prevent. Every OAuth token exchange in this file — client-credentials,
602
+ * auth-code, refresh — funnels through here, so this is the one call site that
603
+ * has to hold.
604
+ *
605
+ * Two behaviours safeFetch adds, both wanted here: a request budget
606
+ * (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
607
+ * that re-checks each hop and drops `Authorization` across an origin boundary.
608
+ * Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
609
+ * per the redirect rules, not a quirk of ours. Real token endpoints do not
610
+ * redirect; one that does was never going to complete the exchange anyway.
611
+ */
525
612
  async postForm(ctx, url, form, headers = {}) {
526
613
  const body = new URLSearchParams();
527
614
  for (const [key, value] of Object.entries(form)) {
@@ -529,7 +616,7 @@ var BaseCredential = class {
529
616
  body.set(key, value);
530
617
  }
531
618
  }
532
- const res = await fetch(url, {
619
+ const res = await safeFetch(url, {
533
620
  method: "POST",
534
621
  signal: ctx.signal,
535
622
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", ...headers },
@@ -713,12 +800,15 @@ export {
713
800
  NodeError,
714
801
  OAuth2AuthCodeCredential,
715
802
  OAuth2ClientCredentialsCredential,
803
+ OPERATORS,
716
804
  RetryableError,
717
805
  SimpleValueCredential,
806
+ VALUELESS_OPERATORS,
718
807
  assertPublicUrl,
719
808
  backoffDelay,
720
809
  buildManifest,
721
810
  clampResponseBytes,
811
+ evaluate,
722
812
  extractCredentialManifest,
723
813
  extractCredentialManifests,
724
814
  extractManifest,
@@ -726,6 +816,7 @@ export {
726
816
  isBlockedAddress,
727
817
  isNodeWithIteration,
728
818
  isOAuthAuthorizeCredential,
819
+ isOperator,
729
820
  maxBytesConfigField,
730
821
  normalizeCredentialType,
731
822
  normalizeLocalized,
@@ -735,7 +826,9 @@ export {
735
826
  readText,
736
827
  retryConfigFields,
737
828
  safeFetch,
829
+ settingApplies,
738
830
  sleepWithSignal,
831
+ takesValue,
739
832
  timeoutConfigField,
740
833
  withRetry
741
834
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/integrations-node-sdk",
3
- "version": "0.18.1",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript interfaces and utilities for Revenexx integration nodes",
5
5
  "license": "MIT",
6
6
  "author": "revenexx GmbH",
@@ -45,6 +45,8 @@
45
45
  "build": "tsup",
46
46
  "dev": "tsup --watch",
47
47
  "test": "node --import tsx --test \"src/**/*.test.ts\"",
48
+ "spec:check": "node scripts/spec-check.mjs",
49
+ "lint": "biome check src",
48
50
  "typecheck": "tsc --noEmit",
49
51
  "prepublishOnly": "npm run build",
50
52
  "release": "changeset publish"
@@ -53,6 +55,7 @@
53
55
  "node": ">=20.3.0"
54
56
  },
55
57
  "devDependencies": {
58
+ "@biomejs/biome": "^2.5.10",
56
59
  "@changesets/cli": "^2.31.0",
57
60
  "@types/node": "^24.0.0",
58
61
  "tsup": "^8.0.0",