@lowdefy/node-utils 0.0.0-experimental-20261006165558 → 0.0.0-experimental-20261007124348
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/createConnectPublic.js +45 -0
- package/dist/createLookupPublicAddress.js +39 -0
- package/dist/index.js +6 -1
- package/dist/isPublicAddress.js +107 -0
- package/dist/journeyGrammar/journeyStepExamples.js +219 -0
- package/dist/journeyGrammar/journeyStepSchema.js +444 -0
- package/dist/journeyGrammar/normaliseJourneySteps.js +31 -0
- package/dist/journeyGrammar/validateJourneySteps.js +34 -1
- package/package.json +5 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { isIP } from 'node:net';
|
|
16
|
+
import { buildConnector } from 'undici';
|
|
17
|
+
import createLookupPublicAddress from './createLookupPublicAddress.js';
|
|
18
|
+
// An undici connector that opens a socket only to a public address, for the first link and every
|
|
19
|
+
// redirect alike. A socket to an IP literal skips the lookup, so a literal is checked here before
|
|
20
|
+
// connecting. A refused address fails the connection with the error createNotPublicError(hostname)
|
|
21
|
+
// returns.
|
|
22
|
+
function createConnectPublic({ createNotPublicError }) {
|
|
23
|
+
const lookupPublicAddress = createLookupPublicAddress({
|
|
24
|
+
createNotPublicError
|
|
25
|
+
});
|
|
26
|
+
const connect = buildConnector({
|
|
27
|
+
lookup: lookupPublicAddress
|
|
28
|
+
});
|
|
29
|
+
return function connectPublic(options, callback) {
|
|
30
|
+
if (isIP(options.hostname) === 0) {
|
|
31
|
+
connect(options, callback);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
lookupPublicAddress(options.hostname, {
|
|
35
|
+
all: true
|
|
36
|
+
}, (error)=>{
|
|
37
|
+
if (error) {
|
|
38
|
+
callback(error, null);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
connect(options, callback);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export default createConnectPublic;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { lookup } from 'node:dns';
|
|
16
|
+
import isPublicAddress from './isPublicAddress.js';
|
|
17
|
+
// A dns.lookup for the socket to use: the socket connects to the addresses this answers, so the
|
|
18
|
+
// address checked is the address connected to, and a name that resolves differently on a second
|
|
19
|
+
// lookup cannot get past the check. Refuses a name when any of its addresses is not public, with
|
|
20
|
+
// the error createNotPublicError(hostname) returns.
|
|
21
|
+
function createLookupPublicAddress({ createNotPublicError }) {
|
|
22
|
+
return function lookupPublicAddress(hostname, options, callback) {
|
|
23
|
+
lookup(hostname, options, (error, address, family)=>{
|
|
24
|
+
if (error) {
|
|
25
|
+
callback(error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const addresses = Array.isArray(address) ? address.map((entry)=>entry.address) : [
|
|
29
|
+
address
|
|
30
|
+
];
|
|
31
|
+
if (!addresses.every(isPublicAddress)) {
|
|
32
|
+
callback(createNotPublicError(hostname));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
callback(null, address, family);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export default createLookupPublicAddress;
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import cleanDirectory from './cleanDirectory.js';
|
|
|
18
18
|
import collectEnvironmentGuards from './collectEnvironmentGuards.js';
|
|
19
19
|
import compareProcessStartTimes from './compareProcessStartTimes.js';
|
|
20
20
|
import copyFileOrDirectory from './copyFileOrDirectory.js';
|
|
21
|
+
import createConnectPublic from './createConnectPublic.js';
|
|
21
22
|
import createClientAddressResolver from './createClientAddressResolver.js';
|
|
23
|
+
import createLookupPublicAddress from './createLookupPublicAddress.js';
|
|
22
24
|
import createSecretScrubber from './createSecretScrubber.js';
|
|
23
25
|
import devPassiveHeader from './devPassiveHeader.js';
|
|
24
26
|
import dataSetNamePattern from './dataSetNamePattern.js';
|
|
@@ -33,6 +35,7 @@ import getSecretsFromEnv from './getSecretsFromEnv.js';
|
|
|
33
35
|
import installIfPackageJsonChanged from './installIfPackageJsonChanged.js';
|
|
34
36
|
import isPidAlive from './isPidAlive.js';
|
|
35
37
|
import isProcessAlive from './isProcessAlive.js';
|
|
38
|
+
import isPublicAddress from './isPublicAddress.js';
|
|
36
39
|
import isProcessStartTime from './isProcessStartTime.js';
|
|
37
40
|
import isPortAvailable from './isPortAvailable.js';
|
|
38
41
|
import linkDependenciesToWorkspace from './linkDependenciesToWorkspace.js';
|
|
@@ -64,6 +67,8 @@ import findPlaceholderStep from './journeyGrammar/findPlaceholderStep.js';
|
|
|
64
67
|
import failurePathKey from './journeyEvidence/failurePathKey.js';
|
|
65
68
|
import isBackedBy from './journeyEvidence/isBackedBy.js';
|
|
66
69
|
import normaliseBlockId from './journeyGrammar/normaliseBlockId.js';
|
|
70
|
+
import normaliseJourneySteps from './journeyGrammar/normaliseJourneySteps.js';
|
|
71
|
+
import journeyStepSchema, { JOURNEY_STEP_SCHEMAS } from './journeyGrammar/journeyStepSchema.js';
|
|
67
72
|
import hashSequence from './journeyCompiler/hashSequence.js';
|
|
68
73
|
import journeySequence from './journeyCompiler/journeySequence.js';
|
|
69
74
|
import listFailurePaths from './journeyCompiler/listFailurePaths.js';
|
|
@@ -78,4 +83,4 @@ import validateJourneySteps, { getStepKey, INTERACTION_STEP_KEYS, STEP_KEYS, TAR
|
|
|
78
83
|
import validateJourneyTags, { JOURNEY_TAG_PATTERN } from './journeyGrammar/validateJourneyTags.js';
|
|
79
84
|
import validateJourneyUser from './journeyGrammar/validateJourneyUser.js';
|
|
80
85
|
import validateTraceRecord from './journeyTrace/validateTraceRecord.js';
|
|
81
|
-
export { acquireMachineSlot, buildSessionReport, checkEnvironmentGuards, cleanDirectory, collectEnvironmentGuards, compareProcessStartTimes, compileSegments, copyFileOrDirectory, countDataSetDocuments, countTextTokens, createClientAddressResolver, createSecretScrubber, devPassiveHeader, dataSetNamePattern, failurePathKey, findAvailablePort, findPlaceholderStep, findPnpmWorkspaceRoot, formatSessionLog, formatSessionReport, findWorkspacePackages, getDevInstancePath, getFileExtension, getFileSubExtension, getLowdefyHome, getProcessStartTime, getSecretsFromEnv, hashSequence, installIfPackageJsonChanged, isBackedBy, isPidAlive, isProcessAlive, isProcessStartTime, isPortAvailable, journeySequence, linkDependenciesToWorkspace, linkWorkspaceDependencies, listDataSets, parseDataSet, listFailurePaths, listRecordingFiles, matchPagePath, normaliseBlockId, normaliseClickText, parseIpRange, parseSince, parsePsStartTime, parseTraceLines, profileProduction, readDevInstance, readDevInstanceAsync, readProcessStartTime, readServerRegistry, registerServer, readRecordings, RECORDING_SOURCES, spawnProcess, stepIdentity, summariseSessions, readFile, writeFile, writeFileAtomic, watchOwner, writeFileIfChanged, getStepKey, INTERACTION_STEP_KEYS, STEP_KEYS, TARGET_KEYS, validateJourneySteps, validateTraceRecord, JOURNEY_TAG_PATTERN, validateJourneyTags, validateJourneyUser };
|
|
86
|
+
export { acquireMachineSlot, buildSessionReport, checkEnvironmentGuards, cleanDirectory, collectEnvironmentGuards, compareProcessStartTimes, compileSegments, copyFileOrDirectory, countDataSetDocuments, countTextTokens, createClientAddressResolver, createConnectPublic, createLookupPublicAddress, createSecretScrubber, devPassiveHeader, dataSetNamePattern, failurePathKey, findAvailablePort, findPlaceholderStep, findPnpmWorkspaceRoot, formatSessionLog, formatSessionReport, findWorkspacePackages, getDevInstancePath, getFileExtension, getFileSubExtension, getLowdefyHome, getProcessStartTime, getSecretsFromEnv, hashSequence, installIfPackageJsonChanged, isBackedBy, isPidAlive, isProcessAlive, isPublicAddress, isProcessStartTime, isPortAvailable, journeySequence, journeyStepSchema, linkDependenciesToWorkspace, linkWorkspaceDependencies, listDataSets, parseDataSet, listFailurePaths, listRecordingFiles, matchPagePath, normaliseBlockId, normaliseJourneySteps, normaliseClickText, parseIpRange, parseSince, parsePsStartTime, parseTraceLines, profileProduction, readDevInstance, readDevInstanceAsync, readProcessStartTime, readServerRegistry, registerServer, readRecordings, RECORDING_SOURCES, spawnProcess, stepIdentity, summariseSessions, readFile, writeFile, writeFileAtomic, watchOwner, writeFileIfChanged, getStepKey, INTERACTION_STEP_KEYS, JOURNEY_STEP_SCHEMAS, STEP_KEYS, TARGET_KEYS, validateJourneySteps, validateTraceRecord, JOURNEY_TAG_PATTERN, validateJourneyTags, validateJourneyUser };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { BlockList, isIPv6 } from 'node:net';
|
|
16
|
+
// Ranges a server-side fetch of a user-supplied link may not reach: anything that is not a public
|
|
17
|
+
// unicast address. BlockList also matches an IPv4-mapped IPv6 address (::ffff:127.0.0.1) against
|
|
18
|
+
// the IPv4 ranges.
|
|
19
|
+
const notPublicIPv4 = [
|
|
20
|
+
[
|
|
21
|
+
'0.0.0.0',
|
|
22
|
+
8
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
'10.0.0.0',
|
|
26
|
+
8
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
'100.64.0.0',
|
|
30
|
+
10
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'127.0.0.0',
|
|
34
|
+
8
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
'169.254.0.0',
|
|
38
|
+
16
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
'172.16.0.0',
|
|
42
|
+
12
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
'192.0.0.0',
|
|
46
|
+
24
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
'192.168.0.0',
|
|
50
|
+
16
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
'198.18.0.0',
|
|
54
|
+
15
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'224.0.0.0',
|
|
58
|
+
4
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
'240.0.0.0',
|
|
62
|
+
4
|
|
63
|
+
]
|
|
64
|
+
];
|
|
65
|
+
// The 6to4 address (2002::/16) that carries an IPv4 network in its next 32 bits.
|
|
66
|
+
function sixToFourNetwork(network) {
|
|
67
|
+
const [a, b, c, d] = network.split('.').map(Number);
|
|
68
|
+
return `2002:${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}::`;
|
|
69
|
+
}
|
|
70
|
+
const notPublic = new BlockList();
|
|
71
|
+
notPublicIPv4.forEach(([network, prefix])=>{
|
|
72
|
+
notPublic.addSubnet(network, prefix, 'ipv4');
|
|
73
|
+
// An IPv6 address that embeds one of these IPv4 addresses: a NAT64 gateway (64:ff9b::/96)
|
|
74
|
+
// or a 6to4 relay forwards it to that IPv4 address.
|
|
75
|
+
notPublic.addSubnet(`64:ff9b::${network}`, 96 + prefix, 'ipv6');
|
|
76
|
+
notPublic.addSubnet(sixToFourNetwork(network), 16 + prefix, 'ipv6');
|
|
77
|
+
});
|
|
78
|
+
[
|
|
79
|
+
[
|
|
80
|
+
'::',
|
|
81
|
+
96
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
'64:ff9b:1::',
|
|
85
|
+
48
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
'fc00::',
|
|
89
|
+
7
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
'fe80::',
|
|
93
|
+
10
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
'fec0::',
|
|
97
|
+
10
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
'ff00::',
|
|
101
|
+
8
|
|
102
|
+
]
|
|
103
|
+
].forEach(([network, prefix])=>notPublic.addSubnet(network, prefix, 'ipv6'));
|
|
104
|
+
function isPublicAddress(address) {
|
|
105
|
+
return !notPublic.check(address, isIPv6(address) ? 'ipv6' : 'ipv4');
|
|
106
|
+
}
|
|
107
|
+
export default isPublicAddress;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ // Well-formed steps, keyed by step and by expect kind. A step error ends with
|
|
16
|
+
// the examples for its step, so an agent that wrote a step wrong gets the
|
|
17
|
+
// right form back; the journey step schema carries the same examples.
|
|
18
|
+
const JOURNEY_STEP_EXAMPLES = {
|
|
19
|
+
click: [
|
|
20
|
+
{
|
|
21
|
+
click: 'save_button'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
click: {
|
|
25
|
+
text: 'OK'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
open: [
|
|
30
|
+
{
|
|
31
|
+
open: 'status_selector'
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
fill: [
|
|
35
|
+
{
|
|
36
|
+
fill: {
|
|
37
|
+
blockId: 'name_input',
|
|
38
|
+
value: 'Ada'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
fill: {
|
|
43
|
+
blockId: 'code_input',
|
|
44
|
+
fromEmail: {
|
|
45
|
+
to: 'ada@example.com',
|
|
46
|
+
match: '\\d{6}'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
select: [
|
|
52
|
+
{
|
|
53
|
+
select: {
|
|
54
|
+
blockId: 'status_selector',
|
|
55
|
+
value: 'Open'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
press: [
|
|
60
|
+
{
|
|
61
|
+
press: 'Enter'
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
back: [
|
|
65
|
+
{
|
|
66
|
+
back: true
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
goto: [
|
|
70
|
+
{
|
|
71
|
+
goto: 'dashboard'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
goto: {
|
|
75
|
+
pageId: 'ticket',
|
|
76
|
+
pathParams: {
|
|
77
|
+
ticket_id: '1234'
|
|
78
|
+
},
|
|
79
|
+
urlQuery: {
|
|
80
|
+
tab: 'notes'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
email: [
|
|
86
|
+
{
|
|
87
|
+
email: {
|
|
88
|
+
to: 'ada@example.com',
|
|
89
|
+
subject: 'Verify'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
as: [
|
|
94
|
+
{
|
|
95
|
+
as: 'invitee'
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
wait: [
|
|
99
|
+
{
|
|
100
|
+
wait: {
|
|
101
|
+
request: 'get_orders'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
wait: {
|
|
106
|
+
state: 'orders'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
wait: {
|
|
111
|
+
ms: 500
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
screenshot: [
|
|
116
|
+
{
|
|
117
|
+
screenshot: 'after_save'
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
expect: [
|
|
121
|
+
{
|
|
122
|
+
expect: {
|
|
123
|
+
visible: 'save_button'
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
expect: {
|
|
128
|
+
url: {
|
|
129
|
+
contains: '/orders'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
'expect.state': [
|
|
135
|
+
{
|
|
136
|
+
expect: {
|
|
137
|
+
state: {
|
|
138
|
+
path: 'order.status',
|
|
139
|
+
equals: 'Open'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
'expect.visible': [
|
|
145
|
+
{
|
|
146
|
+
expect: {
|
|
147
|
+
visible: 'save_button'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
'expect.hidden': [
|
|
152
|
+
{
|
|
153
|
+
expect: {
|
|
154
|
+
hidden: 'error_alert'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
'expect.text': [
|
|
159
|
+
{
|
|
160
|
+
expect: {
|
|
161
|
+
text: {
|
|
162
|
+
blockId: 'title',
|
|
163
|
+
contains: 'Orders'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
'expect.url': [
|
|
169
|
+
{
|
|
170
|
+
expect: {
|
|
171
|
+
url: {
|
|
172
|
+
contains: '/orders'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
'expect.title': [
|
|
178
|
+
{
|
|
179
|
+
expect: {
|
|
180
|
+
title: {
|
|
181
|
+
contains: 'Orders'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
'expect.calls': [
|
|
187
|
+
{
|
|
188
|
+
expect: {
|
|
189
|
+
calls: {
|
|
190
|
+
request: 'save_order',
|
|
191
|
+
count: 1
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
expect: {
|
|
197
|
+
calls: {
|
|
198
|
+
endpoint: 'orders-sync',
|
|
199
|
+
count: 1
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
'expect.error': [
|
|
205
|
+
{
|
|
206
|
+
expect: {
|
|
207
|
+
error: 'is required'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
'expect.effect': [
|
|
212
|
+
{
|
|
213
|
+
expect: {
|
|
214
|
+
effect: true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
export default JOURNEY_STEP_EXAMPLES;
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import JOURNEY_STEP_EXAMPLES from './journeyStepExamples.js';
|
|
16
|
+
// The journey step grammar as a JSON schema, for agents to read. The grammar
|
|
17
|
+
// itself is validateJourneySteps; its tests check this schema accepts and
|
|
18
|
+
// refuses the same steps. Rules across steps (expect.error and expect.effect
|
|
19
|
+
// directly follow an interaction) and a fromEmail match being a valid regular
|
|
20
|
+
// expression are checked by the validator only.
|
|
21
|
+
const nonEmptyString = {
|
|
22
|
+
type: 'string',
|
|
23
|
+
minLength: 1
|
|
24
|
+
};
|
|
25
|
+
const index = {
|
|
26
|
+
type: 'integer',
|
|
27
|
+
minimum: 0
|
|
28
|
+
};
|
|
29
|
+
const FROM = {
|
|
30
|
+
enum: [
|
|
31
|
+
'recorded',
|
|
32
|
+
'shape'
|
|
33
|
+
],
|
|
34
|
+
description: 'Where the value came from: "recorded" (observed in a trace) or "shape" (value: null, a placeholder the runner refuses until it is filled in).'
|
|
35
|
+
};
|
|
36
|
+
const TARGET_PROPERTIES = {
|
|
37
|
+
blockId: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: "Scopes the target to the block's wrapper."
|
|
40
|
+
},
|
|
41
|
+
text: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Exact text of the interactive control to use. Without blockId it searches the whole page, which reaches confirm dialog buttons, modal footers, dropdown menu items and email links.'
|
|
44
|
+
},
|
|
45
|
+
containing: {
|
|
46
|
+
...nonEmptyString,
|
|
47
|
+
description: 'Text the element shows, e.g. the email address a list row shows.'
|
|
48
|
+
},
|
|
49
|
+
row: {
|
|
50
|
+
...index,
|
|
51
|
+
description: 'Zero-based grid row, as displayed. Needs blockId.'
|
|
52
|
+
},
|
|
53
|
+
column: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Grid column id. Needs blockId.'
|
|
56
|
+
},
|
|
57
|
+
nth: {
|
|
58
|
+
...index,
|
|
59
|
+
description: 'Zero-based pick among several matches.'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
function targetObject({ properties = {}, required = [], requireBlockId = false }) {
|
|
63
|
+
const schema = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
...TARGET_PROPERTIES,
|
|
67
|
+
...properties
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
required,
|
|
71
|
+
not: {
|
|
72
|
+
required: [
|
|
73
|
+
'text',
|
|
74
|
+
'containing'
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
dependencies: {
|
|
78
|
+
row: [
|
|
79
|
+
'blockId'
|
|
80
|
+
],
|
|
81
|
+
column: [
|
|
82
|
+
'blockId'
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
if (requireBlockId) {
|
|
87
|
+
schema.required = [
|
|
88
|
+
'blockId',
|
|
89
|
+
...required
|
|
90
|
+
];
|
|
91
|
+
} else {
|
|
92
|
+
schema.anyOf = [
|
|
93
|
+
{
|
|
94
|
+
required: [
|
|
95
|
+
'blockId'
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
required: [
|
|
100
|
+
'text'
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
required: [
|
|
105
|
+
'containing'
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
return schema;
|
|
111
|
+
}
|
|
112
|
+
const TARGET = {
|
|
113
|
+
description: 'A blockId string, or a target object.',
|
|
114
|
+
oneOf: [
|
|
115
|
+
{
|
|
116
|
+
type: 'string'
|
|
117
|
+
},
|
|
118
|
+
targetObject({})
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
// A null value is a placeholder, so it must be marked from: shape.
|
|
122
|
+
const VALUE_PLACEHOLDER_RULE = {
|
|
123
|
+
if: {
|
|
124
|
+
properties: {
|
|
125
|
+
value: {
|
|
126
|
+
type: 'null'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: [
|
|
130
|
+
'value'
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
then: {
|
|
134
|
+
properties: {
|
|
135
|
+
from: {
|
|
136
|
+
const: 'shape'
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
required: [
|
|
140
|
+
'from'
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const FROM_EMAIL = {
|
|
145
|
+
type: 'object',
|
|
146
|
+
description: 'Type text read from the newest email to "to": the first match of the regular expression "match", or its first capture group.',
|
|
147
|
+
properties: {
|
|
148
|
+
to: nonEmptyString,
|
|
149
|
+
subject: {
|
|
150
|
+
type: 'string'
|
|
151
|
+
},
|
|
152
|
+
match: {
|
|
153
|
+
...nonEmptyString,
|
|
154
|
+
description: 'A regular expression, e.g. "\\\\b\\\\d{6}\\\\b".'
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
required: [
|
|
158
|
+
'to',
|
|
159
|
+
'match'
|
|
160
|
+
],
|
|
161
|
+
additionalProperties: false
|
|
162
|
+
};
|
|
163
|
+
function valueTarget() {
|
|
164
|
+
return {
|
|
165
|
+
...targetObject({
|
|
166
|
+
properties: {
|
|
167
|
+
value: {},
|
|
168
|
+
from: FROM
|
|
169
|
+
},
|
|
170
|
+
required: [
|
|
171
|
+
'value'
|
|
172
|
+
],
|
|
173
|
+
requireBlockId: true
|
|
174
|
+
}),
|
|
175
|
+
...VALUE_PLACEHOLDER_RULE
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function singleKey(properties) {
|
|
179
|
+
return Object.entries(properties).map(([key, value])=>({
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
[key]: value
|
|
183
|
+
},
|
|
184
|
+
required: [
|
|
185
|
+
key
|
|
186
|
+
],
|
|
187
|
+
additionalProperties: false
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
const EXPECT_KINDS = {
|
|
191
|
+
state: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
description: 'State at "path" deep-equals "equals".',
|
|
194
|
+
properties: {
|
|
195
|
+
path: {
|
|
196
|
+
type: 'string'
|
|
197
|
+
},
|
|
198
|
+
equals: {},
|
|
199
|
+
from: FROM
|
|
200
|
+
},
|
|
201
|
+
required: [
|
|
202
|
+
'path',
|
|
203
|
+
'equals'
|
|
204
|
+
],
|
|
205
|
+
additionalProperties: false
|
|
206
|
+
},
|
|
207
|
+
visible: TARGET,
|
|
208
|
+
hidden: {
|
|
209
|
+
...TARGET,
|
|
210
|
+
description: 'Nothing the target names is visible. Passes at once when nothing matches yet, so pair it with something that must be present first.'
|
|
211
|
+
},
|
|
212
|
+
text: targetObject({
|
|
213
|
+
properties: {
|
|
214
|
+
contains: {
|
|
215
|
+
type: 'string'
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
required: [
|
|
219
|
+
'contains'
|
|
220
|
+
],
|
|
221
|
+
requireBlockId: true
|
|
222
|
+
}),
|
|
223
|
+
url: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
contains: {
|
|
227
|
+
type: 'string'
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
required: [
|
|
231
|
+
'contains'
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
title: {
|
|
235
|
+
description: 'The document title.',
|
|
236
|
+
oneOf: singleKey({
|
|
237
|
+
equals: {
|
|
238
|
+
type: 'string'
|
|
239
|
+
},
|
|
240
|
+
contains: {
|
|
241
|
+
type: 'string'
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
},
|
|
245
|
+
calls: {
|
|
246
|
+
description: 'How many times this actor called the request (on pageId, default the current page) or the endpoint since the journey started.',
|
|
247
|
+
oneOf: [
|
|
248
|
+
{
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
request: nonEmptyString,
|
|
252
|
+
pageId: nonEmptyString,
|
|
253
|
+
count: index
|
|
254
|
+
},
|
|
255
|
+
required: [
|
|
256
|
+
'request',
|
|
257
|
+
'count'
|
|
258
|
+
],
|
|
259
|
+
additionalProperties: false
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
endpoint: nonEmptyString,
|
|
265
|
+
count: index
|
|
266
|
+
},
|
|
267
|
+
required: [
|
|
268
|
+
'endpoint',
|
|
269
|
+
'count'
|
|
270
|
+
],
|
|
271
|
+
additionalProperties: false
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
},
|
|
275
|
+
error: {
|
|
276
|
+
...nonEmptyString,
|
|
277
|
+
description: 'Text an app error raised by the interaction step directly before this one contains.'
|
|
278
|
+
},
|
|
279
|
+
effect: {
|
|
280
|
+
const: true,
|
|
281
|
+
description: 'Fails when the interaction step directly before this one did nothing: no event ran, the page did not change, no request or endpoint was called and the URL stayed the same.'
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const STEPS = {
|
|
285
|
+
click: {
|
|
286
|
+
description: 'Click a target. "count": 2 or 3 clicks in quick succession, a double click.',
|
|
287
|
+
oneOf: [
|
|
288
|
+
{
|
|
289
|
+
type: 'string'
|
|
290
|
+
},
|
|
291
|
+
targetObject({
|
|
292
|
+
properties: {
|
|
293
|
+
count: {
|
|
294
|
+
type: 'integer',
|
|
295
|
+
minimum: 1,
|
|
296
|
+
maximum: 3
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
open: {
|
|
303
|
+
...TARGET,
|
|
304
|
+
description: "Open an input's dropdown or picker popup and wait for it to show, so a following screenshot captures it."
|
|
305
|
+
},
|
|
306
|
+
fill: {
|
|
307
|
+
description: 'Type a value into a block, or text read from an email.',
|
|
308
|
+
oneOf: [
|
|
309
|
+
valueTarget(),
|
|
310
|
+
targetObject({
|
|
311
|
+
properties: {
|
|
312
|
+
fromEmail: FROM_EMAIL
|
|
313
|
+
},
|
|
314
|
+
required: [
|
|
315
|
+
'fromEmail'
|
|
316
|
+
],
|
|
317
|
+
requireBlockId: true
|
|
318
|
+
})
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
select: {
|
|
322
|
+
...valueTarget(),
|
|
323
|
+
description: 'Pick an option by exact text: a dropdown option, or a radio, button or segmented option in the block.'
|
|
324
|
+
},
|
|
325
|
+
press: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'A key, e.g. "Enter" or "Mod+k" (Mod is Meta or Control per platform).'
|
|
328
|
+
},
|
|
329
|
+
back: {
|
|
330
|
+
enum: [
|
|
331
|
+
true,
|
|
332
|
+
null
|
|
333
|
+
],
|
|
334
|
+
description: 'The browser Back button.'
|
|
335
|
+
},
|
|
336
|
+
goto: {
|
|
337
|
+
description: 'Load an app page like a typed URL.',
|
|
338
|
+
oneOf: [
|
|
339
|
+
nonEmptyString,
|
|
340
|
+
{
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
pageId: nonEmptyString,
|
|
344
|
+
pathParams: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
additionalProperties: {
|
|
347
|
+
type: 'string'
|
|
348
|
+
},
|
|
349
|
+
description: "One string per placeholder of the page's path."
|
|
350
|
+
},
|
|
351
|
+
urlQuery: {
|
|
352
|
+
type: 'object'
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
required: [
|
|
356
|
+
'pageId'
|
|
357
|
+
],
|
|
358
|
+
additionalProperties: false
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
email: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
description: 'Open the newest email to "to" that arrived during this journey, subject containing the text, waiting for it if needed.',
|
|
365
|
+
properties: {
|
|
366
|
+
to: nonEmptyString,
|
|
367
|
+
subject: {
|
|
368
|
+
type: 'string'
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
required: [
|
|
372
|
+
'to'
|
|
373
|
+
],
|
|
374
|
+
additionalProperties: false
|
|
375
|
+
},
|
|
376
|
+
as: {
|
|
377
|
+
...nonEmptyString,
|
|
378
|
+
description: 'Switch to another person, with their own browser and cookies. The journey starts as "main".'
|
|
379
|
+
},
|
|
380
|
+
wait: {
|
|
381
|
+
description: 'Wait for a request started since the last interaction to finish, or for a state path to be defined. { ms } (or a bare number of ms) waits a fixed time, which the lowdefy test lint refuses (L3): prefer a request, a state or an expect.',
|
|
382
|
+
oneOf: [
|
|
383
|
+
{
|
|
384
|
+
type: 'number'
|
|
385
|
+
},
|
|
386
|
+
...singleKey({
|
|
387
|
+
ms: {
|
|
388
|
+
type: 'number'
|
|
389
|
+
},
|
|
390
|
+
request: {
|
|
391
|
+
type: 'string'
|
|
392
|
+
},
|
|
393
|
+
state: {
|
|
394
|
+
type: 'string'
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
]
|
|
398
|
+
},
|
|
399
|
+
screenshot: {
|
|
400
|
+
description: 'Capture the page, with an optional name.',
|
|
401
|
+
anyOf: [
|
|
402
|
+
{
|
|
403
|
+
type: 'string'
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
enum: [
|
|
407
|
+
true,
|
|
408
|
+
null
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
expect: {
|
|
414
|
+
description: 'Assert one thing about the page. Each waits until it holds or the step times out.',
|
|
415
|
+
oneOf: singleKey(EXPECT_KINDS)
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
function stepSchema(key) {
|
|
419
|
+
return {
|
|
420
|
+
type: 'object',
|
|
421
|
+
properties: {
|
|
422
|
+
[key]: {
|
|
423
|
+
...STEPS[key],
|
|
424
|
+
examples: JOURNEY_STEP_EXAMPLES[key]
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
required: [
|
|
428
|
+
key
|
|
429
|
+
],
|
|
430
|
+
additionalProperties: false
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const JOURNEY_STEP_SCHEMAS = Object.fromEntries(Object.keys(STEPS).map((key)=>[
|
|
434
|
+
key,
|
|
435
|
+
stepSchema(key)
|
|
436
|
+
]));
|
|
437
|
+
const journeyStepSchema = {
|
|
438
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
439
|
+
title: 'Journey step',
|
|
440
|
+
description: 'One step of a journey: an object with exactly one key, the step name. A target is a blockId string or an object of { blockId, text, containing, row, column, nth }; fill, select and expect.text need a blockId. expect.error and expect.effect must directly follow an interaction step (click, open, fill, select, press, back).',
|
|
441
|
+
oneOf: Object.values(JOURNEY_STEP_SCHEMAS)
|
|
442
|
+
};
|
|
443
|
+
export { JOURNEY_STEP_SCHEMAS };
|
|
444
|
+
export default journeyStepSchema;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
// Rewrites the grammar's shorthands into their full form, so the runner reads
|
|
17
|
+
// one shape per step: `wait: 3000` becomes `wait: { ms: 3000 }`. Runs on steps
|
|
18
|
+
// validateJourneySteps accepted.
|
|
19
|
+
function normaliseJourneySteps({ steps }) {
|
|
20
|
+
return steps.map((step)=>{
|
|
21
|
+
if (type.isNumber(step.wait)) {
|
|
22
|
+
return {
|
|
23
|
+
wait: {
|
|
24
|
+
ms: step.wait
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return step;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export default normaliseJourneySteps;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import JOURNEY_STEP_EXAMPLES from './journeyStepExamples.js';
|
|
16
17
|
const STEP_KEYS = [
|
|
17
18
|
'click',
|
|
18
19
|
'open',
|
|
@@ -260,7 +261,11 @@ function validateBlockValue({ key, params }) {
|
|
|
260
261
|
requireBlockId: true
|
|
261
262
|
});
|
|
262
263
|
}
|
|
264
|
+
// wait takes { ms }, { request } or { state }; a bare number is read as { ms }.
|
|
263
265
|
function validateWait(params) {
|
|
266
|
+
if (type.isNumber(params)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
264
269
|
if (!type.isObject(params)) {
|
|
265
270
|
return `Step "wait" requires one of { ms }, { request }, { state }. Received ${describe(params)}.`;
|
|
266
271
|
}
|
|
@@ -505,6 +510,22 @@ function validatePlacement({ step, previous }) {
|
|
|
505
510
|
}
|
|
506
511
|
return undefined;
|
|
507
512
|
}
|
|
513
|
+
// The examples an error about this step ends with: the expect kind's own when
|
|
514
|
+
// the step names a known one.
|
|
515
|
+
function getExamplesKey(step) {
|
|
516
|
+
const key = getStepKey(step);
|
|
517
|
+
if (key === 'expect' && type.isObject(step.expect)) {
|
|
518
|
+
const expectKey = getStepKey(step.expect);
|
|
519
|
+
if (EXPECT_KEYS.includes(expectKey)) {
|
|
520
|
+
return `expect.${expectKey}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return key;
|
|
524
|
+
}
|
|
525
|
+
function withExamples({ message, step }) {
|
|
526
|
+
const examples = JOURNEY_STEP_EXAMPLES[getExamplesKey(step)];
|
|
527
|
+
return `${message} Example: ${examples.map(describe).join(' or ')}.`;
|
|
528
|
+
}
|
|
508
529
|
// Validates one step's shape. Returns an error message, or undefined when the
|
|
509
530
|
// step is well-formed.
|
|
510
531
|
function validateStep(step) {
|
|
@@ -516,7 +537,19 @@ function validateStep(step) {
|
|
|
516
537
|
const received = type.isUndefined(key) ? Object.keys(step).join(', ') : key;
|
|
517
538
|
return `Unknown journey step "${received}". Steps are: ${STEP_KEYS.join(', ')}.`;
|
|
518
539
|
}
|
|
519
|
-
const
|
|
540
|
+
const message = validateStepParams({
|
|
541
|
+
key,
|
|
542
|
+
params: step[key]
|
|
543
|
+
});
|
|
544
|
+
if (type.isUndefined(message)) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
return withExamples({
|
|
548
|
+
message,
|
|
549
|
+
step
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function validateStepParams({ key, params }) {
|
|
520
553
|
switch(key){
|
|
521
554
|
case 'click':
|
|
522
555
|
return validateClick(params);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/node-utils",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20261007124348",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -34,9 +34,10 @@
|
|
|
34
34
|
"dist/*"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@lowdefy/errors": "0.0.0-experimental-
|
|
38
|
-
"@lowdefy/helpers": "0.0.0-experimental-
|
|
37
|
+
"@lowdefy/errors": "0.0.0-experimental-20261007124348",
|
|
38
|
+
"@lowdefy/helpers": "0.0.0-experimental-20261007124348",
|
|
39
39
|
"fs-extra": "11.3.4",
|
|
40
|
+
"undici": "7.30.0",
|
|
40
41
|
"yaml": "2.9.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"@swc/cli": "0.8.1",
|
|
45
46
|
"@swc/core": "1.15.32",
|
|
46
47
|
"@swc/jest": "0.2.39",
|
|
48
|
+
"ajv": "8.20.0",
|
|
47
49
|
"copyfiles": "2.4.1",
|
|
48
50
|
"jest": "28.1.3"
|
|
49
51
|
},
|