@revenexx/integrations-node-sdk 0.18.0 → 1.0.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 +2 -1
- package/dist/cli.js +2 -1
- package/dist/index.cjs +102 -3
- package/dist/index.d.cts +215 -3
- package/dist/index.d.ts +215 -3
- package/dist/index.js +96 -3
- package/package.json +4 -1
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(
|
|
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(
|
|
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 *
|
|
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
|
-
/**
|
|
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
|
|
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,6 +163,19 @@ 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
181
|
properties?: IConfigFieldBase[];
|
|
@@ -122,6 +197,14 @@ interface INodeDescription {
|
|
|
122
197
|
/** Associated images (screenshots, logos, banners) shipped with the package. */
|
|
123
198
|
images?: IImage[];
|
|
124
199
|
inputs: Record<string, IInputPort>;
|
|
200
|
+
/**
|
|
201
|
+
* Output ports the workflow editor wires to downstream nodes. An empty array
|
|
202
|
+
* marks a **terminal node**: the path ends here and no edge may leave it —
|
|
203
|
+
* the mirror image of `inputs: {}` on a trigger. `StopAndErrorNode` is the
|
|
204
|
+
* canonical case; it always throws, so there is no success path to wire, and
|
|
205
|
+
* a port that can never fire would leave a dead handle on the canvas. The
|
|
206
|
+
* key stays required so terminality is declared, not forgotten.
|
|
207
|
+
*/
|
|
125
208
|
outputs: IOutputPort[];
|
|
126
209
|
config?: IConfigField[];
|
|
127
210
|
}
|
|
@@ -145,6 +228,101 @@ interface INodeContext {
|
|
|
145
228
|
credentials: {
|
|
146
229
|
get(credentialsId: string): Promise<Record<string, unknown>>;
|
|
147
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* The tenant state store: what this workflow already knows from earlier runs
|
|
233
|
+
* (PO-374). Every call names a namespace the workflow declared in its
|
|
234
|
+
* `state[]` block; anything else is refused by the engine, so a node cannot
|
|
235
|
+
* reach state its workflow did not ask for.
|
|
236
|
+
*/
|
|
237
|
+
state: INodeState;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The four things a workflow can remember between runs, and the reason each is
|
|
241
|
+
* its own operation rather than a generic get/set: the role decides *when* a
|
|
242
|
+
* write becomes visible, and that decision is the engine's to make, not the
|
|
243
|
+
* node author's.
|
|
244
|
+
*
|
|
245
|
+
* The four roles are named **mapping**, **cursor**, **dedupe** and **digest** —
|
|
246
|
+
* those are the values a `state-ref` field's `stateRole` takes. `claim` below is
|
|
247
|
+
* the *operation* on a dedupe namespace, not a role of its own.
|
|
248
|
+
*
|
|
249
|
+
* - **mapping** and **dedupe** take effect immediately. A correlation discarded
|
|
250
|
+
* because the run failed afterwards is how the next run creates a duplicate
|
|
251
|
+
* in the target system; a claim invisible to other runs protects against
|
|
252
|
+
* nothing.
|
|
253
|
+
* - **cursor** and **digest** are staged and adopted only when the run
|
|
254
|
+
* completes. A watermark that advances on a failed run leaves exactly the gap
|
|
255
|
+
* it exists to prevent, and a digest may only count once the write it
|
|
256
|
+
* describes actually went through.
|
|
257
|
+
*
|
|
258
|
+
* At author time — the editor's "test this node" button — the store is
|
|
259
|
+
* **read-only**: a rehearsal must not leave correlations behind that a later
|
|
260
|
+
* production run would treat as truth. The write calls reject there.
|
|
261
|
+
*/
|
|
262
|
+
interface INodeState {
|
|
263
|
+
/** Correlate an id on each side of an integration (PIM article ↔ ERP number). */
|
|
264
|
+
mapping: {
|
|
265
|
+
/**
|
|
266
|
+
* The partner id of a correlation, or `null` when it is unknown — which is
|
|
267
|
+
* the usual way to decide between creating and updating in the target
|
|
268
|
+
* system.
|
|
269
|
+
*
|
|
270
|
+
* `side` says which side of the pair `key` is on, not which side to return:
|
|
271
|
+
* the default `'left'` matches `key` against the left id and answers with
|
|
272
|
+
* the right one, and `'right'` does the reverse. It never searches both, so
|
|
273
|
+
* a lookup on the wrong side answers `null` rather than falling back — and
|
|
274
|
+
* `null` on the create path is what makes the target record a second time.
|
|
275
|
+
*/
|
|
276
|
+
get(namespace: string, key: string, side?: 'left' | 'right'): Promise<string | null>;
|
|
277
|
+
/**
|
|
278
|
+
* Record a correlation. Takes effect immediately, and re-pointing one side
|
|
279
|
+
* of an existing pair is refused: an id correlated two ways is always a bug,
|
|
280
|
+
* and no later sync can untangle it.
|
|
281
|
+
*/
|
|
282
|
+
put(namespace: string, left: string, right: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
283
|
+
};
|
|
284
|
+
/** How far an incremental sync has read. */
|
|
285
|
+
cursor: {
|
|
286
|
+
/**
|
|
287
|
+
* The committed watermark — an arbitrary JSON value such as
|
|
288
|
+
* `{ updatedAfter }` or a provider page token — or `undefined` when the
|
|
289
|
+
* sync has never run. A value staged by *this* run is not visible yet.
|
|
290
|
+
*/
|
|
291
|
+
get(namespace: string, partitionKey?: string): Promise<unknown>;
|
|
292
|
+
/**
|
|
293
|
+
* Stage a watermark. Adopted only if the run completes, so a failed run
|
|
294
|
+
* leaves the previous value in place and the next run picks the gap up.
|
|
295
|
+
* `partitionKey` separates e.g. one shop or company code from another.
|
|
296
|
+
*/
|
|
297
|
+
set(namespace: string, value: unknown, partitionKey?: string): Promise<void>;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Claim a key for the duration of its TTL. `true` means this caller got it;
|
|
301
|
+
* `false` means the key is held and this delivery is a duplicate — the normal
|
|
302
|
+
* answer under at-least-once delivery, and a value to branch on rather than an
|
|
303
|
+
* error.
|
|
304
|
+
*
|
|
305
|
+
* **A claim outlives the attempt that made it.** The holder is not identified:
|
|
306
|
+
* a claim is taken over only once it has expired, so a retry of the very
|
|
307
|
+
* attempt that claimed the key — after a transient failure that happened
|
|
308
|
+
* *after* the claim — is also told `false`. Treating that as a duplicate drops
|
|
309
|
+
* the delivery for the rest of the TTL, which makes `ttlSeconds` the retry
|
|
310
|
+
* window as much as the duplicate-suppression window. Choose it against both,
|
|
311
|
+
* and do not claim earlier in the node than necessary.
|
|
312
|
+
*
|
|
313
|
+
* `ttlSeconds` defaults to 604800 (seven days) and is accepted between 1 and
|
|
314
|
+
* 31536000.
|
|
315
|
+
*/
|
|
316
|
+
claim(namespace: string, key: string, opts?: {
|
|
317
|
+
ttlSeconds?: number;
|
|
318
|
+
}): Promise<boolean>;
|
|
319
|
+
/** Skip expensive writes for entities that have not changed. */
|
|
320
|
+
digest: {
|
|
321
|
+
/** Whether the entity still carries this hash — i.e. the write can be skipped. */
|
|
322
|
+
unchanged(namespace: string, entityKey: string, digest: string): Promise<boolean>;
|
|
323
|
+
/** Stage a hash; adopted only if the run completes. Call it *after* the write. */
|
|
324
|
+
set(namespace: string, entityKey: string, digest: string): Promise<void>;
|
|
325
|
+
};
|
|
148
326
|
}
|
|
149
327
|
interface INodeResult {
|
|
150
328
|
outputs: Record<string, unknown>;
|
|
@@ -412,6 +590,22 @@ interface ITemplateDescription {
|
|
|
412
590
|
*/
|
|
413
591
|
declare function normalizeLocalized(value: LocalizedString | undefined | null, fallbackLang?: string): string | undefined;
|
|
414
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Whether a setting applies, given what the author has filled in so far.
|
|
595
|
+
*
|
|
596
|
+
* A setting with no condition always applies — which is what makes `showIf`
|
|
597
|
+
* additive: a node that says nothing gets the behaviour it has today, and an
|
|
598
|
+
* editor that does not know the key draws every field as before.
|
|
599
|
+
*
|
|
600
|
+
* The value the condition reads is taken from the config as it stands mid-edit,
|
|
601
|
+
* so it is routinely `undefined`: the moment a node is dropped on the canvas
|
|
602
|
+
* nothing is filled in. That is why `equals` against a missing value is `false`
|
|
603
|
+
* rather than an error — the setting does not apply *yet*.
|
|
604
|
+
*/
|
|
605
|
+
declare function settingApplies(field: {
|
|
606
|
+
showIf?: IShowIfCondition;
|
|
607
|
+
}, config: Record<string, unknown>): boolean;
|
|
608
|
+
|
|
415
609
|
/**
|
|
416
610
|
* Reduce an `IConfigField.credentialType` (a single slug or an array of slugs)
|
|
417
611
|
* to a normalized, deduplicated `string[]`.
|
|
@@ -720,7 +914,25 @@ declare abstract class BaseCredential implements ICredential {
|
|
|
720
914
|
abstract readonly description: ICredentialDescription;
|
|
721
915
|
abstract resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
722
916
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
723
|
-
/**
|
|
917
|
+
/**
|
|
918
|
+
* POST `application/x-www-form-urlencoded` and parse a JSON token response.
|
|
919
|
+
*
|
|
920
|
+
* Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
|
|
921
|
+
* endpoint is not a constant: `tokenUrl(config)` derives it from credential
|
|
922
|
+
* config, i.e. from a value someone types into a credential form. A raw fetch
|
|
923
|
+
* would happily POST the client secret at `http://169.254.169.254/…` or any
|
|
924
|
+
* other internal address, which is precisely what `assertPublicUrl` exists to
|
|
925
|
+
* prevent. Every OAuth token exchange in this file — client-credentials,
|
|
926
|
+
* auth-code, refresh — funnels through here, so this is the one call site that
|
|
927
|
+
* has to hold.
|
|
928
|
+
*
|
|
929
|
+
* Two behaviours safeFetch adds, both wanted here: a request budget
|
|
930
|
+
* (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
|
|
931
|
+
* that re-checks each hop and drops `Authorization` across an origin boundary.
|
|
932
|
+
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
933
|
+
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
934
|
+
* redirect; one that does was never going to complete the exchange anyway.
|
|
935
|
+
*/
|
|
724
936
|
protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
|
|
725
937
|
}
|
|
726
938
|
/**
|
|
@@ -796,4 +1008,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
796
1008
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
797
1009
|
}
|
|
798
1010
|
|
|
799
|
-
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 };
|
|
1011
|
+
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,6 +163,19 @@ 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
181
|
properties?: IConfigFieldBase[];
|
|
@@ -122,6 +197,14 @@ interface INodeDescription {
|
|
|
122
197
|
/** Associated images (screenshots, logos, banners) shipped with the package. */
|
|
123
198
|
images?: IImage[];
|
|
124
199
|
inputs: Record<string, IInputPort>;
|
|
200
|
+
/**
|
|
201
|
+
* Output ports the workflow editor wires to downstream nodes. An empty array
|
|
202
|
+
* marks a **terminal node**: the path ends here and no edge may leave it —
|
|
203
|
+
* the mirror image of `inputs: {}` on a trigger. `StopAndErrorNode` is the
|
|
204
|
+
* canonical case; it always throws, so there is no success path to wire, and
|
|
205
|
+
* a port that can never fire would leave a dead handle on the canvas. The
|
|
206
|
+
* key stays required so terminality is declared, not forgotten.
|
|
207
|
+
*/
|
|
125
208
|
outputs: IOutputPort[];
|
|
126
209
|
config?: IConfigField[];
|
|
127
210
|
}
|
|
@@ -145,6 +228,101 @@ interface INodeContext {
|
|
|
145
228
|
credentials: {
|
|
146
229
|
get(credentialsId: string): Promise<Record<string, unknown>>;
|
|
147
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* The tenant state store: what this workflow already knows from earlier runs
|
|
233
|
+
* (PO-374). Every call names a namespace the workflow declared in its
|
|
234
|
+
* `state[]` block; anything else is refused by the engine, so a node cannot
|
|
235
|
+
* reach state its workflow did not ask for.
|
|
236
|
+
*/
|
|
237
|
+
state: INodeState;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The four things a workflow can remember between runs, and the reason each is
|
|
241
|
+
* its own operation rather than a generic get/set: the role decides *when* a
|
|
242
|
+
* write becomes visible, and that decision is the engine's to make, not the
|
|
243
|
+
* node author's.
|
|
244
|
+
*
|
|
245
|
+
* The four roles are named **mapping**, **cursor**, **dedupe** and **digest** —
|
|
246
|
+
* those are the values a `state-ref` field's `stateRole` takes. `claim` below is
|
|
247
|
+
* the *operation* on a dedupe namespace, not a role of its own.
|
|
248
|
+
*
|
|
249
|
+
* - **mapping** and **dedupe** take effect immediately. A correlation discarded
|
|
250
|
+
* because the run failed afterwards is how the next run creates a duplicate
|
|
251
|
+
* in the target system; a claim invisible to other runs protects against
|
|
252
|
+
* nothing.
|
|
253
|
+
* - **cursor** and **digest** are staged and adopted only when the run
|
|
254
|
+
* completes. A watermark that advances on a failed run leaves exactly the gap
|
|
255
|
+
* it exists to prevent, and a digest may only count once the write it
|
|
256
|
+
* describes actually went through.
|
|
257
|
+
*
|
|
258
|
+
* At author time — the editor's "test this node" button — the store is
|
|
259
|
+
* **read-only**: a rehearsal must not leave correlations behind that a later
|
|
260
|
+
* production run would treat as truth. The write calls reject there.
|
|
261
|
+
*/
|
|
262
|
+
interface INodeState {
|
|
263
|
+
/** Correlate an id on each side of an integration (PIM article ↔ ERP number). */
|
|
264
|
+
mapping: {
|
|
265
|
+
/**
|
|
266
|
+
* The partner id of a correlation, or `null` when it is unknown — which is
|
|
267
|
+
* the usual way to decide between creating and updating in the target
|
|
268
|
+
* system.
|
|
269
|
+
*
|
|
270
|
+
* `side` says which side of the pair `key` is on, not which side to return:
|
|
271
|
+
* the default `'left'` matches `key` against the left id and answers with
|
|
272
|
+
* the right one, and `'right'` does the reverse. It never searches both, so
|
|
273
|
+
* a lookup on the wrong side answers `null` rather than falling back — and
|
|
274
|
+
* `null` on the create path is what makes the target record a second time.
|
|
275
|
+
*/
|
|
276
|
+
get(namespace: string, key: string, side?: 'left' | 'right'): Promise<string | null>;
|
|
277
|
+
/**
|
|
278
|
+
* Record a correlation. Takes effect immediately, and re-pointing one side
|
|
279
|
+
* of an existing pair is refused: an id correlated two ways is always a bug,
|
|
280
|
+
* and no later sync can untangle it.
|
|
281
|
+
*/
|
|
282
|
+
put(namespace: string, left: string, right: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
283
|
+
};
|
|
284
|
+
/** How far an incremental sync has read. */
|
|
285
|
+
cursor: {
|
|
286
|
+
/**
|
|
287
|
+
* The committed watermark — an arbitrary JSON value such as
|
|
288
|
+
* `{ updatedAfter }` or a provider page token — or `undefined` when the
|
|
289
|
+
* sync has never run. A value staged by *this* run is not visible yet.
|
|
290
|
+
*/
|
|
291
|
+
get(namespace: string, partitionKey?: string): Promise<unknown>;
|
|
292
|
+
/**
|
|
293
|
+
* Stage a watermark. Adopted only if the run completes, so a failed run
|
|
294
|
+
* leaves the previous value in place and the next run picks the gap up.
|
|
295
|
+
* `partitionKey` separates e.g. one shop or company code from another.
|
|
296
|
+
*/
|
|
297
|
+
set(namespace: string, value: unknown, partitionKey?: string): Promise<void>;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Claim a key for the duration of its TTL. `true` means this caller got it;
|
|
301
|
+
* `false` means the key is held and this delivery is a duplicate — the normal
|
|
302
|
+
* answer under at-least-once delivery, and a value to branch on rather than an
|
|
303
|
+
* error.
|
|
304
|
+
*
|
|
305
|
+
* **A claim outlives the attempt that made it.** The holder is not identified:
|
|
306
|
+
* a claim is taken over only once it has expired, so a retry of the very
|
|
307
|
+
* attempt that claimed the key — after a transient failure that happened
|
|
308
|
+
* *after* the claim — is also told `false`. Treating that as a duplicate drops
|
|
309
|
+
* the delivery for the rest of the TTL, which makes `ttlSeconds` the retry
|
|
310
|
+
* window as much as the duplicate-suppression window. Choose it against both,
|
|
311
|
+
* and do not claim earlier in the node than necessary.
|
|
312
|
+
*
|
|
313
|
+
* `ttlSeconds` defaults to 604800 (seven days) and is accepted between 1 and
|
|
314
|
+
* 31536000.
|
|
315
|
+
*/
|
|
316
|
+
claim(namespace: string, key: string, opts?: {
|
|
317
|
+
ttlSeconds?: number;
|
|
318
|
+
}): Promise<boolean>;
|
|
319
|
+
/** Skip expensive writes for entities that have not changed. */
|
|
320
|
+
digest: {
|
|
321
|
+
/** Whether the entity still carries this hash — i.e. the write can be skipped. */
|
|
322
|
+
unchanged(namespace: string, entityKey: string, digest: string): Promise<boolean>;
|
|
323
|
+
/** Stage a hash; adopted only if the run completes. Call it *after* the write. */
|
|
324
|
+
set(namespace: string, entityKey: string, digest: string): Promise<void>;
|
|
325
|
+
};
|
|
148
326
|
}
|
|
149
327
|
interface INodeResult {
|
|
150
328
|
outputs: Record<string, unknown>;
|
|
@@ -412,6 +590,22 @@ interface ITemplateDescription {
|
|
|
412
590
|
*/
|
|
413
591
|
declare function normalizeLocalized(value: LocalizedString | undefined | null, fallbackLang?: string): string | undefined;
|
|
414
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Whether a setting applies, given what the author has filled in so far.
|
|
595
|
+
*
|
|
596
|
+
* A setting with no condition always applies — which is what makes `showIf`
|
|
597
|
+
* additive: a node that says nothing gets the behaviour it has today, and an
|
|
598
|
+
* editor that does not know the key draws every field as before.
|
|
599
|
+
*
|
|
600
|
+
* The value the condition reads is taken from the config as it stands mid-edit,
|
|
601
|
+
* so it is routinely `undefined`: the moment a node is dropped on the canvas
|
|
602
|
+
* nothing is filled in. That is why `equals` against a missing value is `false`
|
|
603
|
+
* rather than an error — the setting does not apply *yet*.
|
|
604
|
+
*/
|
|
605
|
+
declare function settingApplies(field: {
|
|
606
|
+
showIf?: IShowIfCondition;
|
|
607
|
+
}, config: Record<string, unknown>): boolean;
|
|
608
|
+
|
|
415
609
|
/**
|
|
416
610
|
* Reduce an `IConfigField.credentialType` (a single slug or an array of slugs)
|
|
417
611
|
* to a normalized, deduplicated `string[]`.
|
|
@@ -720,7 +914,25 @@ declare abstract class BaseCredential implements ICredential {
|
|
|
720
914
|
abstract readonly description: ICredentialDescription;
|
|
721
915
|
abstract resolve(ctx: ICredentialContext, config: Config, durableCreds: DurableCreds): Promise<ICredentialResolveResult>;
|
|
722
916
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
723
|
-
/**
|
|
917
|
+
/**
|
|
918
|
+
* POST `application/x-www-form-urlencoded` and parse a JSON token response.
|
|
919
|
+
*
|
|
920
|
+
* Goes through {@link safeFetch}, not a raw `fetch` (PO-185). The token
|
|
921
|
+
* endpoint is not a constant: `tokenUrl(config)` derives it from credential
|
|
922
|
+
* config, i.e. from a value someone types into a credential form. A raw fetch
|
|
923
|
+
* would happily POST the client secret at `http://169.254.169.254/…` or any
|
|
924
|
+
* other internal address, which is precisely what `assertPublicUrl` exists to
|
|
925
|
+
* prevent. Every OAuth token exchange in this file — client-credentials,
|
|
926
|
+
* auth-code, refresh — funnels through here, so this is the one call site that
|
|
927
|
+
* has to hold.
|
|
928
|
+
*
|
|
929
|
+
* Two behaviours safeFetch adds, both wanted here: a request budget
|
|
930
|
+
* (`DEFAULT_TIMEOUT_MS`) on top of `ctx.signal`, and manual redirect handling
|
|
931
|
+
* that re-checks each hop and drops `Authorization` across an origin boundary.
|
|
932
|
+
* Note that a 301/302 answer to this POST is downgraded to a bodiless GET —
|
|
933
|
+
* per the redirect rules, not a quirk of ours. Real token endpoints do not
|
|
934
|
+
* redirect; one that does was never going to complete the exchange anyway.
|
|
935
|
+
*/
|
|
724
936
|
protected postForm(ctx: ICredentialContext, url: string, form: Record<string, string | undefined>, headers?: Record<string, string>): Promise<OAuthTokenResponse>;
|
|
725
937
|
}
|
|
726
938
|
/**
|
|
@@ -796,4 +1008,4 @@ declare abstract class OAuth2AuthCodeCredential extends BaseCredential implement
|
|
|
796
1008
|
test(_ctx: ICredentialContext, config: Config): Promise<ICredentialTestResult>;
|
|
797
1009
|
}
|
|
798
1010
|
|
|
799
|
-
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 };
|
|
1011
|
+
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 *
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.0.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",
|