@showrun/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/__tests__/dsl-validation.test.d.ts +2 -0
- package/dist/__tests__/dsl-validation.test.d.ts.map +1 -0
- package/dist/__tests__/dsl-validation.test.js +203 -0
- package/dist/__tests__/pack-versioning.test.d.ts +2 -0
- package/dist/__tests__/pack-versioning.test.d.ts.map +1 -0
- package/dist/__tests__/pack-versioning.test.js +165 -0
- package/dist/__tests__/validator.test.d.ts +2 -0
- package/dist/__tests__/validator.test.d.ts.map +1 -0
- package/dist/__tests__/validator.test.js +149 -0
- package/dist/authResilience.d.ts +146 -0
- package/dist/authResilience.d.ts.map +1 -0
- package/dist/authResilience.js +378 -0
- package/dist/browserLauncher.d.ts +74 -0
- package/dist/browserLauncher.d.ts.map +1 -0
- package/dist/browserLauncher.js +159 -0
- package/dist/browserPersistence.d.ts +49 -0
- package/dist/browserPersistence.d.ts.map +1 -0
- package/dist/browserPersistence.js +143 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +30 -0
- package/dist/dsl/builders.d.ts +340 -0
- package/dist/dsl/builders.d.ts.map +1 -0
- package/dist/dsl/builders.js +416 -0
- package/dist/dsl/conditions.d.ts +33 -0
- package/dist/dsl/conditions.d.ts.map +1 -0
- package/dist/dsl/conditions.js +169 -0
- package/dist/dsl/interpreter.d.ts +24 -0
- package/dist/dsl/interpreter.d.ts.map +1 -0
- package/dist/dsl/interpreter.js +491 -0
- package/dist/dsl/stepHandlers.d.ts +32 -0
- package/dist/dsl/stepHandlers.d.ts.map +1 -0
- package/dist/dsl/stepHandlers.js +787 -0
- package/dist/dsl/target.d.ts +28 -0
- package/dist/dsl/target.d.ts.map +1 -0
- package/dist/dsl/target.js +110 -0
- package/dist/dsl/templating.d.ts +21 -0
- package/dist/dsl/templating.d.ts.map +1 -0
- package/dist/dsl/templating.js +73 -0
- package/dist/dsl/types.d.ts +695 -0
- package/dist/dsl/types.d.ts.map +1 -0
- package/dist/dsl/types.js +7 -0
- package/dist/dsl/validation.d.ts +15 -0
- package/dist/dsl/validation.d.ts.map +1 -0
- package/dist/dsl/validation.js +974 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/jsonPackValidator.d.ts +11 -0
- package/dist/jsonPackValidator.d.ts.map +1 -0
- package/dist/jsonPackValidator.js +61 -0
- package/dist/loader.d.ts +35 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +107 -0
- package/dist/networkCapture.d.ts +107 -0
- package/dist/networkCapture.d.ts.map +1 -0
- package/dist/networkCapture.js +390 -0
- package/dist/packUtils.d.ts +36 -0
- package/dist/packUtils.d.ts.map +1 -0
- package/dist/packUtils.js +97 -0
- package/dist/packVersioning.d.ts +25 -0
- package/dist/packVersioning.d.ts.map +1 -0
- package/dist/packVersioning.js +137 -0
- package/dist/runner.d.ts +62 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +170 -0
- package/dist/types.d.ts +336 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validator.d.ts +20 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +68 -0
- package/package.json +49 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { validateFlow } from './validation.js';
|
|
2
|
+
import { executeStep } from './stepHandlers.js';
|
|
3
|
+
import { resolveTemplates } from './templating.js';
|
|
4
|
+
import { OnceCache, AuthFailureMonitor, AuthGuardChecker, setupBrowserAuthMonitoring, shouldSkipStep, getOnceSteps, } from '../authResilience.js';
|
|
5
|
+
import { evaluateCondition, conditionToString } from './conditions.js';
|
|
6
|
+
/**
|
|
7
|
+
* Capture the delta of vars and collectibles produced by a step,
|
|
8
|
+
* along with any network entries referenced by new vars (for network_find caching)
|
|
9
|
+
*/
|
|
10
|
+
function captureStepOutputs(varsBefore, varsAfter, collectiblesBefore, collectiblesAfter, networkCapture) {
|
|
11
|
+
const newVars = {};
|
|
12
|
+
const newCollectibles = {};
|
|
13
|
+
const networkEntries = [];
|
|
14
|
+
// Find vars that were added or modified
|
|
15
|
+
for (const [key, value] of Object.entries(varsAfter)) {
|
|
16
|
+
if (!(key in varsBefore) || varsBefore[key] !== value) {
|
|
17
|
+
newVars[key] = value;
|
|
18
|
+
// If the value looks like a request ID (string starting with 'req-'),
|
|
19
|
+
// export the network entry for caching
|
|
20
|
+
if (networkCapture && typeof value === 'string' && value.startsWith('req-')) {
|
|
21
|
+
const entry = networkCapture.exportEntry(value);
|
|
22
|
+
if (entry) {
|
|
23
|
+
networkEntries.push(entry);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Find collectibles that were added or modified
|
|
29
|
+
for (const [key, value] of Object.entries(collectiblesAfter)) {
|
|
30
|
+
if (!(key in collectiblesBefore) || collectiblesBefore[key] !== value) {
|
|
31
|
+
newCollectibles[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
vars: newVars,
|
|
36
|
+
collectibles: newCollectibles,
|
|
37
|
+
networkEntries: networkEntries.length > 0 ? networkEntries : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Redacts secret values from an object before logging.
|
|
42
|
+
* Replaces any occurrence of secret values with [REDACTED].
|
|
43
|
+
*/
|
|
44
|
+
function redactSecrets(obj, secretValues) {
|
|
45
|
+
if (secretValues.length === 0)
|
|
46
|
+
return obj;
|
|
47
|
+
let str = JSON.stringify(obj);
|
|
48
|
+
for (const value of secretValues) {
|
|
49
|
+
// Only redact secrets that are at least 3 characters long
|
|
50
|
+
if (value && value.length >= 3) {
|
|
51
|
+
// Escape special regex characters in the secret value
|
|
52
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
53
|
+
str = str.replace(new RegExp(escaped, 'g'), '[REDACTED]');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(str);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return obj;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Runs a flow of DSL steps sequentially with auth resilience support
|
|
65
|
+
*/
|
|
66
|
+
export async function runFlow(ctx, steps, options) {
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
const stopOnError = options?.stopOnError ?? true;
|
|
69
|
+
const inputs = options?.inputs ?? {};
|
|
70
|
+
const authConfig = options?.auth;
|
|
71
|
+
const sessionId = options?.sessionId;
|
|
72
|
+
const profileId = options?.profileId;
|
|
73
|
+
const cacheDir = options?.cacheDir;
|
|
74
|
+
const secrets = options?.secrets ?? {};
|
|
75
|
+
// Get secret values for redaction (only values >= 3 chars)
|
|
76
|
+
const secretValues = Object.values(secrets).filter((v) => v && v.length >= 3);
|
|
77
|
+
// Validate flow before execution
|
|
78
|
+
validateFlow(steps);
|
|
79
|
+
const collectibles = {};
|
|
80
|
+
const vars = {};
|
|
81
|
+
// Initialize variable context (including secrets for templating)
|
|
82
|
+
const variableContext = {
|
|
83
|
+
inputs,
|
|
84
|
+
vars,
|
|
85
|
+
secrets,
|
|
86
|
+
};
|
|
87
|
+
// Initialize auth resilience components: load persisted "once" cache when sessionId/profileId provided
|
|
88
|
+
const onceCache = sessionId || profileId
|
|
89
|
+
? OnceCache.fromDisk(sessionId, profileId, cacheDir)
|
|
90
|
+
: new OnceCache();
|
|
91
|
+
let authMonitor = null;
|
|
92
|
+
let authGuard = null;
|
|
93
|
+
let authRecoveriesUsed = 0;
|
|
94
|
+
const stepContext = {
|
|
95
|
+
page: ctx.page,
|
|
96
|
+
collectibles,
|
|
97
|
+
vars,
|
|
98
|
+
inputs,
|
|
99
|
+
networkCapture: ctx.networkCapture,
|
|
100
|
+
authMonitor: authMonitor ?? undefined,
|
|
101
|
+
};
|
|
102
|
+
if (authConfig?.authPolicy) {
|
|
103
|
+
authMonitor = new AuthFailureMonitor(authConfig.authPolicy);
|
|
104
|
+
if (authMonitor.isEnabled()) {
|
|
105
|
+
setupBrowserAuthMonitoring(ctx.page, authMonitor, ctx.logger);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (authConfig?.authGuard) {
|
|
109
|
+
authGuard = new AuthGuardChecker(authConfig.authGuard);
|
|
110
|
+
// Run auth guard check if enabled (before main steps)
|
|
111
|
+
if (authGuard.isEnabled()) {
|
|
112
|
+
const authValid = await authGuard.checkAuth(ctx.page);
|
|
113
|
+
if (!authValid) {
|
|
114
|
+
// Auth guard failed - run setup steps (once steps) before main flow
|
|
115
|
+
const onceSteps = getOnceSteps(steps);
|
|
116
|
+
if (onceSteps.length > 0) {
|
|
117
|
+
await executeStepsWithRecovery(ctx, stepContext, variableContext, onceSteps, onceCache, authMonitor, sessionId, profileId, stopOnError, authRecoveriesUsed);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let stepsExecuted = 0;
|
|
123
|
+
try {
|
|
124
|
+
// Execute steps sequentially
|
|
125
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
126
|
+
const step = steps[stepIndex];
|
|
127
|
+
const stepStartTime = Date.now();
|
|
128
|
+
const stepLabel = step.label || step.id;
|
|
129
|
+
// Check if step should be skipped due to "once" cache
|
|
130
|
+
if (shouldSkipStep(step, onceCache, sessionId, profileId)) {
|
|
131
|
+
// Determine scope and restore cached outputs
|
|
132
|
+
const scope = step.once === 'session' && sessionId ? 'session' : 'profile';
|
|
133
|
+
const cachedOutputs = onceCache.getOutputs(step.id, scope);
|
|
134
|
+
// Restore cached vars and collectibles
|
|
135
|
+
if (cachedOutputs) {
|
|
136
|
+
Object.assign(vars, cachedOutputs.vars);
|
|
137
|
+
Object.assign(collectibles, cachedOutputs.collectibles);
|
|
138
|
+
// Restore network entries into the capture buffer
|
|
139
|
+
if (cachedOutputs.networkEntries && ctx.networkCapture) {
|
|
140
|
+
for (const entry of cachedOutputs.networkEntries) {
|
|
141
|
+
ctx.networkCapture.importEntry(entry);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
ctx.logger.log({
|
|
146
|
+
type: 'step_skipped',
|
|
147
|
+
data: {
|
|
148
|
+
stepId: step.id,
|
|
149
|
+
type: step.type,
|
|
150
|
+
reason: 'once_already_executed',
|
|
151
|
+
restoredVars: cachedOutputs ? Object.keys(cachedOutputs.vars) : [],
|
|
152
|
+
restoredCollectibles: cachedOutputs ? Object.keys(cachedOutputs.collectibles) : [],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
stepsExecuted++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Check if step should be skipped due to skip_if condition
|
|
159
|
+
if (step.skip_if) {
|
|
160
|
+
try {
|
|
161
|
+
const shouldSkip = await evaluateCondition({ page: ctx.page, vars }, step.skip_if);
|
|
162
|
+
if (shouldSkip) {
|
|
163
|
+
ctx.logger.log({
|
|
164
|
+
type: 'step_skipped',
|
|
165
|
+
data: {
|
|
166
|
+
stepId: step.id,
|
|
167
|
+
type: step.type,
|
|
168
|
+
reason: 'condition_met',
|
|
169
|
+
condition: conditionToString(step.skip_if),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
stepsExecuted++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (conditionError) {
|
|
177
|
+
// Log condition evaluation error but continue with step execution
|
|
178
|
+
console.warn(`[interpreter] Error evaluating skip_if for step ${step.id}:`, conditionError);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Resolve templates in step params before execution
|
|
182
|
+
const resolvedStep = {
|
|
183
|
+
...step,
|
|
184
|
+
params: resolveTemplates(step.params, variableContext),
|
|
185
|
+
};
|
|
186
|
+
// Log step start with resolved params (redact secrets for safe logging)
|
|
187
|
+
const logParams = redactSecrets(JSON.parse(JSON.stringify(resolvedStep.params)), secretValues);
|
|
188
|
+
ctx.logger.log({
|
|
189
|
+
type: 'step_started',
|
|
190
|
+
data: {
|
|
191
|
+
stepId: step.id,
|
|
192
|
+
type: step.type,
|
|
193
|
+
label: stepLabel,
|
|
194
|
+
params: logParams,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
// Snapshot state before step execution (for capturing outputs)
|
|
199
|
+
const varsBefore = resolvedStep.once ? { ...vars } : {};
|
|
200
|
+
const collectiblesBefore = resolvedStep.once ? { ...collectibles } : {};
|
|
201
|
+
// Apply step-level timeout if specified
|
|
202
|
+
const timeoutMs = resolvedStep.timeoutMs;
|
|
203
|
+
let stepPromise = executeStep(stepContext, resolvedStep);
|
|
204
|
+
if (timeoutMs) {
|
|
205
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
reject(new Error(`Step timeout after ${timeoutMs}ms`));
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
});
|
|
210
|
+
stepPromise = Promise.race([stepPromise, timeoutPromise]);
|
|
211
|
+
}
|
|
212
|
+
await stepPromise;
|
|
213
|
+
stepsExecuted++;
|
|
214
|
+
// Mark step as executed if it has "once" flag, with captured outputs
|
|
215
|
+
if (resolvedStep.once) {
|
|
216
|
+
const scope = resolvedStep.once === 'session' && sessionId ? 'session' : 'profile';
|
|
217
|
+
const stepOutputs = captureStepOutputs(varsBefore, vars, collectiblesBefore, collectibles, ctx.networkCapture);
|
|
218
|
+
onceCache.markExecuted(step.id, scope, stepOutputs);
|
|
219
|
+
}
|
|
220
|
+
// Log step finish
|
|
221
|
+
const stepDuration = Date.now() - stepStartTime;
|
|
222
|
+
ctx.logger.log({
|
|
223
|
+
type: 'step_finished',
|
|
224
|
+
data: {
|
|
225
|
+
stepId: step.id,
|
|
226
|
+
type: step.type,
|
|
227
|
+
label: stepLabel,
|
|
228
|
+
durationMs: stepDuration,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
234
|
+
// Check for auth failure and attempt recovery
|
|
235
|
+
if (authMonitor?.isEnabled()) {
|
|
236
|
+
const stepFailures = authMonitor.getFailuresForStep(step.id);
|
|
237
|
+
if (stepFailures.length > 0) {
|
|
238
|
+
// Auth failure detected for this step - attempt recovery
|
|
239
|
+
const latestFailure = stepFailures[stepFailures.length - 1];
|
|
240
|
+
authRecoveriesUsed++;
|
|
241
|
+
const maxRecoveries = authMonitor.getMaxRecoveries();
|
|
242
|
+
if (authRecoveriesUsed > maxRecoveries) {
|
|
243
|
+
// Recovery exhausted
|
|
244
|
+
ctx.logger.log({
|
|
245
|
+
type: 'auth_recovery_exhausted',
|
|
246
|
+
data: {
|
|
247
|
+
url: latestFailure.url,
|
|
248
|
+
status: latestFailure.status,
|
|
249
|
+
maxRecoveries,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
throw new Error(`Auth recovery exhausted after ${maxRecoveries} attempt(s). Last failure: ${latestFailure.status} at ${latestFailure.url}`);
|
|
253
|
+
}
|
|
254
|
+
// Attempt recovery
|
|
255
|
+
ctx.logger.log({
|
|
256
|
+
type: 'auth_recovery_started',
|
|
257
|
+
data: {
|
|
258
|
+
recoveryAttempt: authRecoveriesUsed,
|
|
259
|
+
maxRecoveries,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
// Clear once cache based on pack scope (sessionId/profileId)
|
|
263
|
+
// If sessionId is provided, clear session cache; otherwise clear profile cache
|
|
264
|
+
// Clear both if both are provided (to be safe)
|
|
265
|
+
if (sessionId && profileId) {
|
|
266
|
+
onceCache.clearAll();
|
|
267
|
+
}
|
|
268
|
+
else if (sessionId) {
|
|
269
|
+
onceCache.clear('session');
|
|
270
|
+
}
|
|
271
|
+
else if (profileId) {
|
|
272
|
+
onceCache.clear('profile');
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// No scope provided - clear both to be safe
|
|
276
|
+
onceCache.clearAll();
|
|
277
|
+
}
|
|
278
|
+
// Rerun once steps (setup)
|
|
279
|
+
const onceSteps = getOnceSteps(steps);
|
|
280
|
+
if (onceSteps.length > 0) {
|
|
281
|
+
await executeStepsWithRecovery(ctx, stepContext, variableContext, onceSteps, onceCache, authMonitor, sessionId, profileId, stopOnError, authRecoveriesUsed);
|
|
282
|
+
}
|
|
283
|
+
// Clear the failure record for this step (to allow retry)
|
|
284
|
+
authMonitor.clearFailuresForStep(step.id);
|
|
285
|
+
// Wait for cooldown if configured
|
|
286
|
+
const cooldownMs = authMonitor.getCooldownMs();
|
|
287
|
+
if (cooldownMs > 0) {
|
|
288
|
+
await new Promise((resolve) => setTimeout(resolve, cooldownMs));
|
|
289
|
+
}
|
|
290
|
+
// Retry the failed step (up to maxStepRetryAfterRecovery times)
|
|
291
|
+
const maxRetries = authMonitor.getMaxStepRetries();
|
|
292
|
+
const timeoutMs = resolvedStep.timeoutMs; // Capture timeoutMs for retry loop
|
|
293
|
+
let retrySuccess = false;
|
|
294
|
+
for (let retryAttempt = 0; retryAttempt < maxRetries; retryAttempt++) {
|
|
295
|
+
try {
|
|
296
|
+
// Snapshot state before retry (for capturing outputs)
|
|
297
|
+
const retryVarsBefore = resolvedStep.once ? { ...vars } : {};
|
|
298
|
+
const retryCollectiblesBefore = resolvedStep.once ? { ...collectibles } : {};
|
|
299
|
+
// Update current step ID for monitoring
|
|
300
|
+
stepContext.currentStepId = step.id;
|
|
301
|
+
setupBrowserAuthMonitoring(ctx.page, authMonitor, ctx.logger, step.id);
|
|
302
|
+
let retryPromise = executeStep(stepContext, resolvedStep);
|
|
303
|
+
if (timeoutMs) {
|
|
304
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
reject(new Error(`Step timeout after ${timeoutMs}ms`));
|
|
307
|
+
}, timeoutMs);
|
|
308
|
+
});
|
|
309
|
+
retryPromise = Promise.race([retryPromise, timeoutPromise]);
|
|
310
|
+
}
|
|
311
|
+
await retryPromise;
|
|
312
|
+
retrySuccess = true;
|
|
313
|
+
stepsExecuted++;
|
|
314
|
+
// Mark step as executed if it has "once" flag, with captured outputs
|
|
315
|
+
if (resolvedStep.once) {
|
|
316
|
+
const scope = resolvedStep.once === 'session' && sessionId ? 'session' : 'profile';
|
|
317
|
+
const stepOutputs = captureStepOutputs(retryVarsBefore, vars, retryCollectiblesBefore, collectibles, ctx.networkCapture);
|
|
318
|
+
onceCache.markExecuted(step.id, scope, stepOutputs);
|
|
319
|
+
}
|
|
320
|
+
// Log successful retry
|
|
321
|
+
const stepDuration = Date.now() - stepStartTime;
|
|
322
|
+
ctx.logger.log({
|
|
323
|
+
type: 'step_finished',
|
|
324
|
+
data: {
|
|
325
|
+
stepId: step.id,
|
|
326
|
+
type: step.type,
|
|
327
|
+
label: stepLabel,
|
|
328
|
+
durationMs: stepDuration,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
ctx.logger.log({
|
|
332
|
+
type: 'auth_recovery_finished',
|
|
333
|
+
data: {
|
|
334
|
+
recoveryAttempt: authRecoveriesUsed,
|
|
335
|
+
success: true,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
break; // Success - exit retry loop
|
|
339
|
+
}
|
|
340
|
+
catch (retryError) {
|
|
341
|
+
// Retry failed - check if we have more attempts
|
|
342
|
+
if (retryAttempt === maxRetries - 1) {
|
|
343
|
+
// All retries exhausted
|
|
344
|
+
ctx.logger.log({
|
|
345
|
+
type: 'auth_recovery_finished',
|
|
346
|
+
data: {
|
|
347
|
+
recoveryAttempt: authRecoveriesUsed,
|
|
348
|
+
success: false,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
throw retryError;
|
|
352
|
+
}
|
|
353
|
+
// Wait before next retry
|
|
354
|
+
if (cooldownMs > 0) {
|
|
355
|
+
await new Promise((resolve) => setTimeout(resolve, cooldownMs));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (retrySuccess) {
|
|
360
|
+
continue; // Step succeeded after recovery - continue to next step
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Log error
|
|
365
|
+
ctx.logger.log({
|
|
366
|
+
type: 'error',
|
|
367
|
+
data: {
|
|
368
|
+
error: errorMessage,
|
|
369
|
+
stepId: step.id,
|
|
370
|
+
type: step.type,
|
|
371
|
+
label: stepLabel,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
// Handle optional steps
|
|
375
|
+
if (resolvedStep.optional) {
|
|
376
|
+
// Optional step failed - log and continue
|
|
377
|
+
stepsExecuted++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// Handle per-step error behavior
|
|
381
|
+
const errorBehavior = resolvedStep.onError || (stopOnError ? 'stop' : 'continue');
|
|
382
|
+
if (errorBehavior === 'continue') {
|
|
383
|
+
// Continue to next step
|
|
384
|
+
stepsExecuted++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// Capture artifacts on error if available
|
|
388
|
+
if (ctx.artifacts) {
|
|
389
|
+
try {
|
|
390
|
+
await ctx.artifacts.saveScreenshot(`error-${step.id}`);
|
|
391
|
+
const html = await ctx.page.content();
|
|
392
|
+
await ctx.artifacts.saveHTML(`error-${step.id}`, html);
|
|
393
|
+
}
|
|
394
|
+
catch (artifactError) {
|
|
395
|
+
// Ignore artifact save errors, but log them
|
|
396
|
+
console.error('Failed to save artifacts:', artifactError);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Stop on error (default behavior)
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const durationMs = Date.now() - startTime;
|
|
404
|
+
const finalUrl = ctx.page.url();
|
|
405
|
+
// Collect JMESPath hints from vars (stored by step handlers)
|
|
406
|
+
const jmespathHints = vars['__jmespath_hints'] || [];
|
|
407
|
+
const singleHint = vars['__jmespath_hint'];
|
|
408
|
+
if (singleHint && !jmespathHints.includes(singleHint)) {
|
|
409
|
+
jmespathHints.push(singleHint);
|
|
410
|
+
}
|
|
411
|
+
// Deduplicate hints
|
|
412
|
+
const uniqueHints = [...new Set(jmespathHints)];
|
|
413
|
+
const result = {
|
|
414
|
+
collectibles,
|
|
415
|
+
meta: {
|
|
416
|
+
url: finalUrl,
|
|
417
|
+
durationMs,
|
|
418
|
+
stepsExecuted,
|
|
419
|
+
stepsTotal: steps.length,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
// Only include _hints if there are any
|
|
423
|
+
if (uniqueHints.length > 0) {
|
|
424
|
+
result._hints = uniqueHints;
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
if (sessionId || profileId) {
|
|
433
|
+
onceCache.persist(sessionId, profileId, cacheDir);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Helper to execute steps with recovery support (for rerunning once steps)
|
|
439
|
+
*/
|
|
440
|
+
async function executeStepsWithRecovery(ctx, stepContext, variableContext, steps, onceCache, authMonitor, sessionId, profileId, stopOnError, authRecoveriesUsed) {
|
|
441
|
+
const { vars } = variableContext;
|
|
442
|
+
const collectibles = stepContext.collectibles;
|
|
443
|
+
const networkCapture = stepContext.networkCapture;
|
|
444
|
+
for (const step of steps) {
|
|
445
|
+
// Skip if already executed (shouldn't happen during recovery, but be safe)
|
|
446
|
+
if (shouldSkipStep(step, onceCache, sessionId, profileId)) {
|
|
447
|
+
// Restore cached outputs when skipping during recovery
|
|
448
|
+
const scope = step.once === 'session' && sessionId ? 'session' : 'profile';
|
|
449
|
+
const cachedOutputs = onceCache.getOutputs(step.id, scope);
|
|
450
|
+
if (cachedOutputs) {
|
|
451
|
+
Object.assign(vars, cachedOutputs.vars);
|
|
452
|
+
Object.assign(collectibles, cachedOutputs.collectibles);
|
|
453
|
+
// Restore network entries into the capture buffer
|
|
454
|
+
if (cachedOutputs.networkEntries && networkCapture) {
|
|
455
|
+
for (const entry of cachedOutputs.networkEntries) {
|
|
456
|
+
networkCapture.importEntry(entry);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const resolvedStep = {
|
|
463
|
+
...step,
|
|
464
|
+
params: resolveTemplates(step.params, variableContext),
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
// Snapshot state before step execution (for capturing outputs)
|
|
468
|
+
const varsBefore = resolvedStep.once ? { ...vars } : {};
|
|
469
|
+
const collectiblesBefore = resolvedStep.once ? { ...collectibles } : {};
|
|
470
|
+
// Update current step ID and monitoring for current step
|
|
471
|
+
stepContext.currentStepId = step.id;
|
|
472
|
+
if (authMonitor?.isEnabled()) {
|
|
473
|
+
setupBrowserAuthMonitoring(ctx.page, authMonitor, ctx.logger, step.id);
|
|
474
|
+
}
|
|
475
|
+
await executeStep(stepContext, resolvedStep);
|
|
476
|
+
// Mark as executed with captured outputs
|
|
477
|
+
if (resolvedStep.once) {
|
|
478
|
+
const scope = resolvedStep.once === 'session' && sessionId ? 'session' : 'profile';
|
|
479
|
+
const stepOutputs = captureStepOutputs(varsBefore, vars, collectiblesBefore, collectibles, networkCapture);
|
|
480
|
+
onceCache.markExecuted(step.id, scope, stepOutputs);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
// If recovery fails during setup rerun, throw (don't retry recovery)
|
|
485
|
+
if (stopOnError) {
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
// Otherwise continue
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Page, BrowserContext, Frame } from 'playwright';
|
|
2
|
+
import type { DslStep } from './types.js';
|
|
3
|
+
import type { NetworkCaptureApi } from '../networkCapture.js';
|
|
4
|
+
import type { AuthFailureMonitor } from '../authResilience.js';
|
|
5
|
+
/**
|
|
6
|
+
* Step execution context
|
|
7
|
+
*/
|
|
8
|
+
export interface StepContext {
|
|
9
|
+
page: Page;
|
|
10
|
+
collectibles: Record<string, unknown>;
|
|
11
|
+
vars: Record<string, unknown>;
|
|
12
|
+
inputs: Record<string, unknown>;
|
|
13
|
+
/** Required for network_find and network_replay */
|
|
14
|
+
networkCapture?: NetworkCaptureApi;
|
|
15
|
+
/** Optional auth failure monitor for detecting auth failures in network_replay */
|
|
16
|
+
authMonitor?: AuthFailureMonitor;
|
|
17
|
+
/** Current step ID for auth failure tracking */
|
|
18
|
+
currentStepId?: string;
|
|
19
|
+
/** Browser context for multi-tab operations */
|
|
20
|
+
browserContext?: BrowserContext;
|
|
21
|
+
/** Current frame context (for iframe operations) */
|
|
22
|
+
currentFrame?: Frame;
|
|
23
|
+
/** Previous tab index (for switch_tab with 'previous') */
|
|
24
|
+
previousTabIndex?: number;
|
|
25
|
+
/** Task pack directory path (for resolving relative file paths) */
|
|
26
|
+
packDir?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Executes a single DSL step
|
|
30
|
+
*/
|
|
31
|
+
export declare function executeStep(ctx: StepContext, step: DslStep): Promise<void>;
|
|
32
|
+
//# sourceMappingURL=stepHandlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stepHandlers.d.ts","sourceRoot":"","sources":["../../src/dsl/stepHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EACV,OAAO,EAqBR,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,sBAAsB,CAAC;AAGxG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,mDAAmD;IACnD,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC,kFAAkF;IAClF,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAw5BD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC,CAgEf"}
|