@flowriki/agent 0.1.0-beta.26 → 0.1.0-beta.28
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/{chunk-IH7KFCV4.js → chunk-PETFNOJA.js} +510 -19
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +3 -1
|
@@ -6,7 +6,7 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
9
|
+
var AGENT_VERSION = "0.1.0-beta.28";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -133,9 +133,9 @@ function forgetConnection(service, env = process.env) {
|
|
|
133
133
|
fs3.renameSync(temporary, file);
|
|
134
134
|
return true;
|
|
135
135
|
}
|
|
136
|
-
var fingerprint = (
|
|
136
|
+
var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
|
|
137
137
|
|
|
138
|
-
// ../contracts/dist/chunk-
|
|
138
|
+
// ../contracts/dist/chunk-UFMRFPRU.js
|
|
139
139
|
import { z } from "zod";
|
|
140
140
|
var PROTOCOL_VERSION = 1;
|
|
141
141
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -293,7 +293,10 @@ var AGENT_FEATURES = /* @__PURE__ */ new Set([
|
|
|
293
293
|
"actors.v1",
|
|
294
294
|
/** Replaying other flows' steps before a recording starts capturing, so a
|
|
295
295
|
* flow can be recorded from where its prerequisites leave the browser. */
|
|
296
|
-
"prerequisites.v1"
|
|
296
|
+
"prerequisites.v1",
|
|
297
|
+
/** Running a helper — an email code, an authenticator, a text, an API
|
|
298
|
+
* call — as a step, from the helpers a run is sent. */
|
|
299
|
+
"helpers.v1"
|
|
297
300
|
]);
|
|
298
301
|
var AgentMessageSchema = z.discriminatedUnion("type", [
|
|
299
302
|
AckSchema,
|
|
@@ -461,7 +464,7 @@ import path7 from "path";
|
|
|
461
464
|
// ../engine-playwright/dist/index.js
|
|
462
465
|
import path3 from "path";
|
|
463
466
|
|
|
464
|
-
// ../test-model/dist/chunk-
|
|
467
|
+
// ../test-model/dist/chunk-GALEMMJW.js
|
|
465
468
|
import { z as z3 } from "zod";
|
|
466
469
|
var LocatorOverrideSchema = z3.object({
|
|
467
470
|
strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
|
|
@@ -667,7 +670,7 @@ var DialogAnswerSchema = z3.object({
|
|
|
667
670
|
}).strict();
|
|
668
671
|
var RecordedStepSchema = RecordedTargetSchema.extend({
|
|
669
672
|
id: z3.string().min(1),
|
|
670
|
-
type: z3.enum(["fill", "select", "click", "dblclick", "rightclick", "drag", "check", "press", "hover", "upload", "scroll", "back", "forward", "navigate", "closePage", "unsupported", "setRange", "assert", "signIn", "signOut", "useActor", "resetActor", "capture"]),
|
|
673
|
+
type: z3.enum(["fill", "select", "click", "dblclick", "rightclick", "drag", "check", "press", "hover", "upload", "scroll", "back", "forward", "navigate", "closePage", "unsupported", "setRange", "assert", "signIn", "signOut", "useActor", "resetActor", "capture", "useHelper"]),
|
|
671
674
|
scope: z3.string(),
|
|
672
675
|
value: z3.string(),
|
|
673
676
|
secret: z3.boolean(),
|
|
@@ -743,7 +746,21 @@ var RecordedStepSchema = RecordedTargetSchema.extend({
|
|
|
743
746
|
* participants cost a login each time, which is the cost this removes. */
|
|
744
747
|
actorId: z3.string().optional(),
|
|
745
748
|
navigationBase: z3.enum(["page", "application"]).optional(),
|
|
746
|
-
capture: z3.object({ name: z3.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/), source: z3.enum(["text", "value", "url"]) }).strict().optional()
|
|
749
|
+
capture: z3.object({ name: z3.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/), source: z3.enum(["text", "value", "url"]) }).strict().optional(),
|
|
750
|
+
/** WHICH HELPER, for `useHelper`: the email code, the authenticator, the
|
|
751
|
+
* text, the API call — set up on the Helpers screen and referred to here by
|
|
752
|
+
* id, never copied, so fixing a helper fixes every test using it.
|
|
753
|
+
*
|
|
754
|
+
* `inputs` fills the helper's `{{names}}` and may itself use a value read
|
|
755
|
+
* earlier in the run (`{{email}}`). What comes back is kept under `saveAs`
|
|
756
|
+
* for later steps, exactly as a captured value is; `then: "open"` also
|
|
757
|
+
* takes the browser to it, which is what a verification link is for. */
|
|
758
|
+
helper: z3.object({
|
|
759
|
+
helperId: z3.string(),
|
|
760
|
+
inputs: z3.record(z3.string(), z3.string()).default({}),
|
|
761
|
+
saveAs: z3.string().regex(/^([a-zA-Z][a-zA-Z0-9_]*)?$/),
|
|
762
|
+
then: z3.enum(["remember", "open"]).default("remember")
|
|
763
|
+
}).strict().optional()
|
|
747
764
|
}).strict();
|
|
748
765
|
var DraftStepSchema = RecordedStepSchema.extend({
|
|
749
766
|
role: z3.string().default(""),
|
|
@@ -940,6 +957,7 @@ function liftSignInFirst(raw) {
|
|
|
940
957
|
import { z as z4 } from "zod";
|
|
941
958
|
import { z as z22 } from "zod";
|
|
942
959
|
import { z as z32 } from "zod";
|
|
960
|
+
import { z as z42 } from "zod";
|
|
943
961
|
var EngineSchema = z4.enum(["chromium", "firefox", "webkit"]);
|
|
944
962
|
var CapabilitySchema = z4.object({
|
|
945
963
|
id: z4.string(),
|
|
@@ -1590,6 +1608,115 @@ function expandForRun(flow, lookup) {
|
|
|
1590
1608
|
const { signIn, steps } = preparationFor(flow, lookup);
|
|
1591
1609
|
return { ...rest, steps: [...signIn ? [signIn] : [], ...steps, ...flow.steps] };
|
|
1592
1610
|
}
|
|
1611
|
+
var HELPER_SCHEMA_VERSION = 1;
|
|
1612
|
+
var SecretRef = z42.string().regex(/^[a-z][a-z0-9_]*$/i);
|
|
1613
|
+
var PatternSchema = z42.string().max(500).refine((source) => {
|
|
1614
|
+
try {
|
|
1615
|
+
new RegExp(source);
|
|
1616
|
+
return true;
|
|
1617
|
+
} catch {
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
}, "that is not a valid pattern");
|
|
1621
|
+
var MailboxHelperSchema = z42.object({
|
|
1622
|
+
kind: z42.literal("mailbox"),
|
|
1623
|
+
/** IMAP, which every provider offers: Gmail, Outlook, Zoho, Fastmail… */
|
|
1624
|
+
host: z42.string().min(1),
|
|
1625
|
+
port: z42.number().int().positive().default(993),
|
|
1626
|
+
secure: z42.boolean().default(true),
|
|
1627
|
+
username: z42.string().min(1),
|
|
1628
|
+
passwordRef: SecretRef,
|
|
1629
|
+
folder: z42.string().min(1).default("INBOX"),
|
|
1630
|
+
/** Which email. All optional; the newest that matches every one given. */
|
|
1631
|
+
match: z42.object({
|
|
1632
|
+
to: z42.string().default(""),
|
|
1633
|
+
from: z42.string().default(""),
|
|
1634
|
+
subject: z42.string().default("")
|
|
1635
|
+
}).strict().default({}),
|
|
1636
|
+
/** What to take out of it. */
|
|
1637
|
+
extract: z42.object({
|
|
1638
|
+
take: z42.enum(["code", "link", "subject", "body"]).default("code"),
|
|
1639
|
+
/** For a code: the pattern, first group if it has one. Default: 4–8 digits.
|
|
1640
|
+
* For a link: only links containing this text. */
|
|
1641
|
+
pattern: z42.string().max(500).default("")
|
|
1642
|
+
}).strict().default({}),
|
|
1643
|
+
/** How long to wait for the email to arrive. */
|
|
1644
|
+
waitSeconds: z42.number().int().min(0).max(300).default(60)
|
|
1645
|
+
}).strict();
|
|
1646
|
+
var TotpHelperSchema = z42.object({
|
|
1647
|
+
kind: z42.literal("totp"),
|
|
1648
|
+
/** The authenticator secret (base32, from the QR code's "manual entry"). */
|
|
1649
|
+
secretRef: SecretRef,
|
|
1650
|
+
digits: z42.union([z42.literal(6), z42.literal(8)]).default(6),
|
|
1651
|
+
period: z42.number().int().min(10).max(120).default(30),
|
|
1652
|
+
algorithm: z42.enum(["SHA1", "SHA256", "SHA512"]).default("SHA1")
|
|
1653
|
+
}).strict();
|
|
1654
|
+
var SmsHelperSchema = z42.object({
|
|
1655
|
+
kind: z42.literal("sms"),
|
|
1656
|
+
provider: z42.literal("twilio").default("twilio"),
|
|
1657
|
+
accountSid: z42.string().regex(/^AC[0-9a-f]{32}$/i, "a Twilio account SID starts with AC followed by 32 characters"),
|
|
1658
|
+
authTokenRef: SecretRef,
|
|
1659
|
+
/** The Twilio number the application texts. */
|
|
1660
|
+
to: z42.string().min(3),
|
|
1661
|
+
match: z42.object({
|
|
1662
|
+
from: z42.string().default(""),
|
|
1663
|
+
contains: z42.string().default("")
|
|
1664
|
+
}).strict().default({}),
|
|
1665
|
+
extract: z42.object({ pattern: z42.string().max(500).default("") }).strict().default({}),
|
|
1666
|
+
waitSeconds: z42.number().int().min(0).max(300).default(60)
|
|
1667
|
+
}).strict();
|
|
1668
|
+
var HttpHelperSchema = z42.object({
|
|
1669
|
+
kind: z42.literal("http"),
|
|
1670
|
+
method: z42.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("POST"),
|
|
1671
|
+
url: z42.string().min(1),
|
|
1672
|
+
/** Plain headers, in the repository. */
|
|
1673
|
+
headers: z42.record(z42.string(), z42.string()).default({}),
|
|
1674
|
+
/** Header name → vault reference, for tokens and keys. */
|
|
1675
|
+
secretHeaders: z42.record(z42.string(), SecretRef).default({}),
|
|
1676
|
+
body: z42.string().max(2e4).default(""),
|
|
1677
|
+
/** Refused unless the response has this status. 0 means any 2xx. */
|
|
1678
|
+
expectStatus: z42.number().int().min(0).max(599).default(0),
|
|
1679
|
+
extract: z42.object({
|
|
1680
|
+
take: z42.enum(["json", "pattern", "status", "nothing"]).default("nothing"),
|
|
1681
|
+
/** For json: a dot path — `data.order.id`, `items.0.code`. */
|
|
1682
|
+
path: z42.string().max(300).default(""),
|
|
1683
|
+
pattern: z42.string().max(500).default("")
|
|
1684
|
+
}).strict().default({})
|
|
1685
|
+
}).strict();
|
|
1686
|
+
var HelperConfigSchema = z42.discriminatedUnion("kind", [
|
|
1687
|
+
MailboxHelperSchema,
|
|
1688
|
+
TotpHelperSchema,
|
|
1689
|
+
SmsHelperSchema,
|
|
1690
|
+
HttpHelperSchema
|
|
1691
|
+
]);
|
|
1692
|
+
var HelperSchema = z42.object({
|
|
1693
|
+
schemaVersion: z42.literal(HELPER_SCHEMA_VERSION),
|
|
1694
|
+
id: z42.string().min(1),
|
|
1695
|
+
slug: z42.string().regex(/^[a-z0-9][a-z0-9-]*$/),
|
|
1696
|
+
name: z42.string().min(1).max(120),
|
|
1697
|
+
description: z42.string().max(1e3).default(""),
|
|
1698
|
+
config: HelperConfigSchema,
|
|
1699
|
+
createdAt: z42.string(),
|
|
1700
|
+
createdBy: z42.string().default(""),
|
|
1701
|
+
updatedAt: z42.string(),
|
|
1702
|
+
revision: z42.number().int().nonnegative().default(0)
|
|
1703
|
+
}).strict().superRefine((helper, ctx) => {
|
|
1704
|
+
const patterns = [
|
|
1705
|
+
helper.config.kind === "mailbox" && helper.config.extract.take === "code" ? helper.config.extract.pattern : "",
|
|
1706
|
+
helper.config.kind === "sms" ? helper.config.extract.pattern : "",
|
|
1707
|
+
helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : ""
|
|
1708
|
+
];
|
|
1709
|
+
for (const pattern of patterns) {
|
|
1710
|
+
if (pattern && !PatternSchema.safeParse(pattern).success) {
|
|
1711
|
+
ctx.addIssue({ code: "custom", path: ["config", "extract", "pattern"], message: "that is not a valid pattern" });
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
var DeliveredHelperSchema = z42.object({
|
|
1716
|
+
id: z42.string().min(1),
|
|
1717
|
+
name: z42.string(),
|
|
1718
|
+
config: HelperConfigSchema
|
|
1719
|
+
}).strict();
|
|
1593
1720
|
|
|
1594
1721
|
// ../engine-playwright/dist/index.js
|
|
1595
1722
|
import { createHmac, randomBytes } from "crypto";
|
|
@@ -3390,6 +3517,27 @@ function withRuntimeValues(step, values) {
|
|
|
3390
3517
|
proposals: step.proposals.map((check) => ({ ...check, value: Array.isArray(check.value) ? check.value.map(expand) : expand(check.value) }))
|
|
3391
3518
|
};
|
|
3392
3519
|
}
|
|
3520
|
+
var NAME = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
|
|
3521
|
+
async function performHelperStep(page, step, options) {
|
|
3522
|
+
const helper = step.helper;
|
|
3523
|
+
if (!helper?.helperId) throw new Error("Choose which helper this step uses");
|
|
3524
|
+
if (!helper.saveAs) throw new Error("Name the value this step gets");
|
|
3525
|
+
if (!options.runHelper) {
|
|
3526
|
+
throw new Error("helpers run only on runs started from the dashboard \u2014 this machine was not sent any");
|
|
3527
|
+
}
|
|
3528
|
+
const inputs = Object.fromEntries(Object.entries(helper.inputs ?? {}).map(([name, text]) => [name, text.replace(NAME, (_, used) => {
|
|
3529
|
+
if (!options.values.has(used)) throw new Error(`Runtime value "${used}" has not been captured yet`);
|
|
3530
|
+
return options.values.get(used);
|
|
3531
|
+
})]));
|
|
3532
|
+
const got = await options.runHelper(helper.helperId, inputs);
|
|
3533
|
+
if (got.value.length > 16384) throw new Error("The helper's value is over 16 KB");
|
|
3534
|
+
options.values.set(helper.saveAs, got.value);
|
|
3535
|
+
if (helper.then === "open") {
|
|
3536
|
+
if (!/^https?:\/\//i.test(got.value)) throw new Error(`The helper gave "${got.value.slice(0, 80)}", which is not a link to open`);
|
|
3537
|
+
await page.goto(got.value, { waitUntil: "domcontentloaded", timeout: options.navigationTimeoutMs ?? 45e3 });
|
|
3538
|
+
}
|
|
3539
|
+
return got;
|
|
3540
|
+
}
|
|
3393
3541
|
var secretsBehind = (auth) => [.../* @__PURE__ */ new Set([
|
|
3394
3542
|
...Object.values(auth.secretRefs ?? {}),
|
|
3395
3543
|
...auth.steps.flatMap((step) => step.secretRef ? [step.secretRef] : [])
|
|
@@ -3625,7 +3773,9 @@ var RunActors = class {
|
|
|
3625
3773
|
pages: session.pages,
|
|
3626
3774
|
baseUrl: this.opts.baseUrl,
|
|
3627
3775
|
...this.opts.resolveAuth ? { resolveAuth: this.opts.resolveAuth } : {},
|
|
3628
|
-
...this.opts.resolveSecret ? { resolveSecret: this.opts.resolveSecret } : {}
|
|
3776
|
+
...this.opts.resolveSecret ? { resolveSecret: this.opts.resolveSecret } : {},
|
|
3777
|
+
...this.opts.runHelper ? { runHelper: this.opts.runHelper } : {},
|
|
3778
|
+
values: this.values
|
|
3629
3779
|
}
|
|
3630
3780
|
);
|
|
3631
3781
|
if (!this.preparations.has(id)) this.preparations.set(id, "fresh");
|
|
@@ -4095,6 +4245,11 @@ async function locateCandidates(page, step, opts = {}) {
|
|
|
4095
4245
|
async function locateAll(page, step, opts = {}) {
|
|
4096
4246
|
return fromPlan(page, step, locatorPlan(step), opts);
|
|
4097
4247
|
}
|
|
4248
|
+
var ICON_GLYPHS = "[\\s\\uE000-\\uF8FF\\u{F0000}-\\u{FFFFD}\\u{100000}-\\u{10FFFD}]*";
|
|
4249
|
+
function nameIgnoringIcons(name) {
|
|
4250
|
+
const escaped = name.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
4251
|
+
return new RegExp(`^${ICON_GLYPHS}${escaped}${ICON_GLYPHS}$`, "u");
|
|
4252
|
+
}
|
|
4098
4253
|
async function fromPlan(page, step, plan, opts = {}) {
|
|
4099
4254
|
let root = rootFor(page, step);
|
|
4100
4255
|
if ((step.scope || step.scopeRole) && !plan.overridden) {
|
|
@@ -4112,7 +4267,7 @@ async function fromPlan(page, step, plan, opts = {}) {
|
|
|
4112
4267
|
loc = root.getByTestId(plan.arg);
|
|
4113
4268
|
break;
|
|
4114
4269
|
case "getByText":
|
|
4115
|
-
loc = root.getByText(plan.arg, { exact: plan.exact });
|
|
4270
|
+
loc = root.getByText(plan.exact ? nameIgnoringIcons(plan.arg) : plan.arg, { exact: plan.exact });
|
|
4116
4271
|
break;
|
|
4117
4272
|
case "getByLabel":
|
|
4118
4273
|
loc = root.getByLabel(plan.arg, { exact: true });
|
|
@@ -4127,7 +4282,12 @@ async function fromPlan(page, step, plan, opts = {}) {
|
|
|
4127
4282
|
loc = root.getByTitle(plan.arg, { exact: true });
|
|
4128
4283
|
break;
|
|
4129
4284
|
case "getByRole":
|
|
4130
|
-
loc = root.getByRole(plan.arg, {
|
|
4285
|
+
loc = root.getByRole(plan.arg, {
|
|
4286
|
+
// Unchanged when there is no name to match, or an empty one.
|
|
4287
|
+
name: plan.name ? nameIgnoringIcons(plan.name) : plan.name,
|
|
4288
|
+
exact: true,
|
|
4289
|
+
includeHidden: opts.includeHidden
|
|
4290
|
+
});
|
|
4131
4291
|
break;
|
|
4132
4292
|
case "none":
|
|
4133
4293
|
if (opts.allowRoleOnly && step.role) {
|
|
@@ -4476,7 +4636,7 @@ async function openRunSession(opts) {
|
|
|
4476
4636
|
const consoleLines = [];
|
|
4477
4637
|
if (artifacts.console) {
|
|
4478
4638
|
const typed = steps.filter((step) => step.secret).map((step) => resolveSecret?.(step.secretRef || step.value)).filter((value) => Boolean(value && value.length >= 3));
|
|
4479
|
-
const redact2 = (line) => typed.reduce((text,
|
|
4639
|
+
const redact2 = (line) => typed.reduce((text, secret2) => text.split(secret2).join("[secret]"), line);
|
|
4480
4640
|
const watch = (p) => p.on("console", (m) => {
|
|
4481
4641
|
if (consoleLines.length < 2e3) consoleLines.push(redact2(`[${m.type()}] ${m.text()}`));
|
|
4482
4642
|
});
|
|
@@ -4626,6 +4786,7 @@ async function replayInto(target, steps, opts) {
|
|
|
4626
4786
|
startUrl: null,
|
|
4627
4787
|
...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
|
|
4628
4788
|
...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
|
|
4789
|
+
...opts.runHelper ? { runHelper: opts.runHelper } : {},
|
|
4629
4790
|
...opts.baseUrl ? { baseUrl: opts.baseUrl } : {}
|
|
4630
4791
|
});
|
|
4631
4792
|
return outcome.status === "passed" ? { ok: true } : { ok: false, error: outcome.error ?? "a step before recording did not complete" };
|
|
@@ -4653,6 +4814,7 @@ function stepFailure(step, detail) {
|
|
|
4653
4814
|
async function runStepsIn(session, steps, opts) {
|
|
4654
4815
|
let { page, pages, config, artifacts } = session;
|
|
4655
4816
|
const { resolveSecret, resolveFile, onStepResult, evidenceDir } = opts;
|
|
4817
|
+
const values = opts.actors?.values ?? opts.values ?? /* @__PURE__ */ new Map();
|
|
4656
4818
|
const results = [];
|
|
4657
4819
|
let status2 = "passed";
|
|
4658
4820
|
let error = null;
|
|
@@ -4664,7 +4826,7 @@ async function runStepsIn(session, steps, opts) {
|
|
|
4664
4826
|
let step = recorded;
|
|
4665
4827
|
if (!recorded.disabled) {
|
|
4666
4828
|
try {
|
|
4667
|
-
step = withRuntimeValues(recorded,
|
|
4829
|
+
step = withRuntimeValues(recorded, values);
|
|
4668
4830
|
} catch (err) {
|
|
4669
4831
|
const result2 = stepFailure(recorded, firstLine2(err));
|
|
4670
4832
|
results.push(result2);
|
|
@@ -4849,6 +5011,14 @@ async function runStepsIn(session, steps, opts) {
|
|
|
4849
5011
|
valuesResolved: true,
|
|
4850
5012
|
baseUrl: opts.baseUrl,
|
|
4851
5013
|
...opts.actors ? { actors: opts.actors } : {},
|
|
5014
|
+
...opts.runHelper ? { runHelper: opts.runHelper } : {},
|
|
5015
|
+
values,
|
|
5016
|
+
// What the helper got is what this row reports: the code typed next
|
|
5017
|
+
// is the first thing anybody checks when the application refused it.
|
|
5018
|
+
onHelper: (got) => {
|
|
5019
|
+
result.value = got.value;
|
|
5020
|
+
result.detail = got.detail;
|
|
5021
|
+
},
|
|
4852
5022
|
timeoutMs: config.timeoutMs,
|
|
4853
5023
|
navigationTimeoutMs: config.navigationTimeoutMs,
|
|
4854
5024
|
// SAID OUT LOUD, on a step that passed. The element was found, but
|
|
@@ -4958,6 +5128,7 @@ async function replayFlow(opts) {
|
|
|
4958
5128
|
...opts.evidenceDir === void 0 ? {} : { evidenceDir: opts.evidenceDir },
|
|
4959
5129
|
...opts.signal === void 0 ? {} : { signal: opts.signal },
|
|
4960
5130
|
...opts.resolveAuth === void 0 ? {} : { resolveAuth: opts.resolveAuth },
|
|
5131
|
+
...opts.runHelper === void 0 ? {} : { runHelper: opts.runHelper },
|
|
4961
5132
|
baseUrl,
|
|
4962
5133
|
startUrl: null
|
|
4963
5134
|
});
|
|
@@ -4981,7 +5152,8 @@ async function replayFlow(opts) {
|
|
|
4981
5152
|
}
|
|
4982
5153
|
async function executeStep(main, input, options = {}) {
|
|
4983
5154
|
const parsed = RecordedStepSchema.parse(input);
|
|
4984
|
-
const
|
|
5155
|
+
const values = options.actors?.values ?? options.values ?? /* @__PURE__ */ new Map();
|
|
5156
|
+
const step = options.valuesResolved ? parsed : withRuntimeValues(parsed, values);
|
|
4985
5157
|
if (options.actors) {
|
|
4986
5158
|
if (step.type === "resetActor") {
|
|
4987
5159
|
await options.actors.freshGuest(step.actorId ?? options.actors.currentId);
|
|
@@ -5010,6 +5182,15 @@ async function executeStep(main, input, options = {}) {
|
|
|
5010
5182
|
if (!outcome.ok) throw new Error(outcome.detail ?? "Authentication failed");
|
|
5011
5183
|
return main;
|
|
5012
5184
|
}
|
|
5185
|
+
if (step.type === "useHelper") {
|
|
5186
|
+
const got = await performHelperStep(page, step, {
|
|
5187
|
+
values,
|
|
5188
|
+
...options.runHelper ? { runHelper: options.runHelper } : {},
|
|
5189
|
+
...options.navigationTimeoutMs ? { navigationTimeoutMs: options.navigationTimeoutMs } : {}
|
|
5190
|
+
});
|
|
5191
|
+
options.onHelper?.(got);
|
|
5192
|
+
return page;
|
|
5193
|
+
}
|
|
5013
5194
|
if (step.type === "capture") {
|
|
5014
5195
|
if (!step.capture || !options.actors) throw new Error("Configure the runtime output name and source");
|
|
5015
5196
|
if (step.secret) throw new Error("Secret fields cannot be captured as shared outputs");
|
|
@@ -5233,6 +5414,7 @@ async function replayCase(opts) {
|
|
|
5233
5414
|
actors,
|
|
5234
5415
|
baseUrl: occurrence.baseUrl ?? baseUrl,
|
|
5235
5416
|
...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
|
|
5417
|
+
...opts.runHelper ? { runHelper: opts.runHelper } : {},
|
|
5236
5418
|
stepOffset: outcomes.reduce((count, previous) => count + previous.stepsTotal, 0),
|
|
5237
5419
|
...occurrence.ownSignIn ? { ownSignIn: occurrence.ownSignIn } : {},
|
|
5238
5420
|
...opts.resolveSecret === void 0 ? {} : { resolveSecret: opts.resolveSecret },
|
|
@@ -5690,6 +5872,7 @@ async function startRecordingSession(opts) {
|
|
|
5690
5872
|
const replayed = await replayInto({ browser, context, page, config }, opts.preparation.steps, {
|
|
5691
5873
|
resolveSecret: opts.preparation.resolveSecret,
|
|
5692
5874
|
...opts.preparation.resolveAuth ? { resolveAuth: opts.preparation.resolveAuth } : {},
|
|
5875
|
+
...opts.preparation.runHelper ? { runHelper: opts.preparation.runHelper } : {},
|
|
5693
5876
|
baseUrl: url
|
|
5694
5877
|
});
|
|
5695
5878
|
if (!replayed.ok) throw new Error(replayed.error);
|
|
@@ -5870,7 +6053,7 @@ async function startRecordingSession(opts) {
|
|
|
5870
6053
|
function firstLine2(err) {
|
|
5871
6054
|
return String(err?.message ?? err).split("\n")[0].trim();
|
|
5872
6055
|
}
|
|
5873
|
-
var SESSION_STEPS = /* @__PURE__ */ new Set(["signIn", "signOut", "useActor", "resetActor", "capture"]);
|
|
6056
|
+
var SESSION_STEPS = /* @__PURE__ */ new Set(["signIn", "signOut", "useActor", "resetActor", "capture", "useHelper"]);
|
|
5874
6057
|
var usesAnOutput = (text) => Array.isArray(text) ? text.some(usesAnOutput) : String(text ?? "").includes("{{");
|
|
5875
6058
|
function needsWholeRun(opts) {
|
|
5876
6059
|
if (opts.actors?.length) return true;
|
|
@@ -6223,7 +6406,7 @@ async function startDebugSession(opts) {
|
|
|
6223
6406
|
step = withRuntimeValues(step, actors?.values ?? /* @__PURE__ */ new Map());
|
|
6224
6407
|
const fresh = step.secret && recordedInSession.has(step.id);
|
|
6225
6408
|
const on = actors?.current ?? { page, pages };
|
|
6226
|
-
const active = await executeStep(on.page, fresh ? { ...step, secret: false } : step, { pages: on.pages, resolveSecret, resolveFile, timeoutMs, baseUrl, valuesResolved: true, ...actors ? { actors } : {}, ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {} });
|
|
6409
|
+
const active = await executeStep(on.page, fresh ? { ...step, secret: false } : step, { pages: on.pages, resolveSecret, resolveFile, timeoutMs, baseUrl, valuesResolved: true, ...actors ? { actors } : {}, ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {}, ...opts.runHelper ? { runHelper: opts.runHelper } : {} });
|
|
6227
6410
|
for (const a of step.proposals ?? []) {
|
|
6228
6411
|
if (a.enabled === false) continue;
|
|
6229
6412
|
const label = describeAssertion(a, step.name);
|
|
@@ -7297,7 +7480,7 @@ function toSummary(row) {
|
|
|
7297
7480
|
};
|
|
7298
7481
|
}
|
|
7299
7482
|
|
|
7300
|
-
// ../repository-store/dist/chunk-
|
|
7483
|
+
// ../repository-store/dist/chunk-HTFXAD4R.js
|
|
7301
7484
|
var FLOWRIKI_DIR = ".flowriki";
|
|
7302
7485
|
var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
|
|
7303
7486
|
var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
|
|
@@ -7308,6 +7491,7 @@ var CASES_DIR = `${FLOWRIKI_DIR}/cases`;
|
|
|
7308
7491
|
var SUITES_DIR = `${FLOWRIKI_DIR}/suites`;
|
|
7309
7492
|
var AUTH_DIR = `${FLOWRIKI_DIR}/auth`;
|
|
7310
7493
|
var ACCOUNTS_DIR = `${AUTH_DIR}/accounts`;
|
|
7494
|
+
var HELPERS_DIR = `${FLOWRIKI_DIR}/helpers`;
|
|
7311
7495
|
function assertSlug(slug) {
|
|
7312
7496
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid slug: ${slug}`);
|
|
7313
7497
|
return slug;
|
|
@@ -8016,6 +8200,275 @@ function ensureRootCommit(root) {
|
|
|
8016
8200
|
return git2(root, ["rev-list", "--max-parents=0", "HEAD"]).split("\n").pop();
|
|
8017
8201
|
}
|
|
8018
8202
|
|
|
8203
|
+
// ../helpers/dist/index.js
|
|
8204
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
8205
|
+
var HelperFailure = class extends Error {
|
|
8206
|
+
constructor(message) {
|
|
8207
|
+
super(message);
|
|
8208
|
+
this.name = "HelperFailure";
|
|
8209
|
+
}
|
|
8210
|
+
};
|
|
8211
|
+
var NAME2 = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
|
|
8212
|
+
function fill(text, inputs = {}) {
|
|
8213
|
+
return text.replace(NAME2, (_, name) => {
|
|
8214
|
+
if (inputs[name] === void 0) throw new HelperFailure(`needs a value for {{${name}}}`);
|
|
8215
|
+
return inputs[name];
|
|
8216
|
+
});
|
|
8217
|
+
}
|
|
8218
|
+
var secret = (ctx, ref, what) => {
|
|
8219
|
+
const value = ctx.resolveSecret(ref);
|
|
8220
|
+
if (!value) throw new HelperFailure(`the ${what} has not been set \u2014 save it on this helper first`);
|
|
8221
|
+
return value;
|
|
8222
|
+
};
|
|
8223
|
+
var DEFAULT_CODE = "\\b\\d{4,8}\\b";
|
|
8224
|
+
function takeCode(text, pattern) {
|
|
8225
|
+
const re = new RegExp(pattern || DEFAULT_CODE);
|
|
8226
|
+
const m = re.exec(text);
|
|
8227
|
+
if (!m) return null;
|
|
8228
|
+
return (m[1] ?? m[0]).trim();
|
|
8229
|
+
}
|
|
8230
|
+
function linksIn(html, text) {
|
|
8231
|
+
const out = [];
|
|
8232
|
+
for (const m of html.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) out.push(m[1].replace(/&/g, "&"));
|
|
8233
|
+
for (const m of text.matchAll(/https?:\/\/[^\s<>"')\]]+/g)) out.push(m[0]);
|
|
8234
|
+
return [...new Set(out)].filter((url) => /^https?:\/\//i.test(url));
|
|
8235
|
+
}
|
|
8236
|
+
var textOfHtml = (html) => html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/\s+/g, " ").trim();
|
|
8237
|
+
var ago = (from, at) => {
|
|
8238
|
+
const s2 = Math.max(0, Math.round((from - at.getTime()) / 1e3));
|
|
8239
|
+
return s2 < 60 ? `${s2}s ago` : s2 < 3600 ? `${Math.round(s2 / 60)} min ago` : at.toISOString();
|
|
8240
|
+
};
|
|
8241
|
+
function base32(input) {
|
|
8242
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
8243
|
+
const clean = input.toUpperCase().replace(/[\s=-]/g, "");
|
|
8244
|
+
let bits = 0, value = 0;
|
|
8245
|
+
const out = [];
|
|
8246
|
+
for (const ch of clean) {
|
|
8247
|
+
const at = alphabet.indexOf(ch);
|
|
8248
|
+
if (at < 0) throw new HelperFailure(`the authenticator secret is not base32 \u2014 copy it from the "can't scan the code?" option`);
|
|
8249
|
+
value = value << 5 | at;
|
|
8250
|
+
bits += 5;
|
|
8251
|
+
if (bits >= 8) {
|
|
8252
|
+
out.push(value >>> bits - 8 & 255);
|
|
8253
|
+
bits -= 8;
|
|
8254
|
+
}
|
|
8255
|
+
}
|
|
8256
|
+
return Buffer.from(out);
|
|
8257
|
+
}
|
|
8258
|
+
function totp(secretBase32, at, options = {}) {
|
|
8259
|
+
const { digits = 6, period = 30, algorithm = "SHA1" } = options;
|
|
8260
|
+
const counter = Math.floor(at / 1e3 / period);
|
|
8261
|
+
const buffer = Buffer.alloc(8);
|
|
8262
|
+
buffer.writeBigUInt64BE(BigInt(counter));
|
|
8263
|
+
const hmac = createHmac2(algorithm.toLowerCase(), base32(secretBase32)).update(buffer).digest();
|
|
8264
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
8265
|
+
const binary = (hmac[offset] & 127) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3];
|
|
8266
|
+
const code = String(binary % 10 ** digits).padStart(digits, "0");
|
|
8267
|
+
const remaining = period - Math.floor(at / 1e3 % period);
|
|
8268
|
+
return { code, remaining };
|
|
8269
|
+
}
|
|
8270
|
+
async function imapMailbox(config, password) {
|
|
8271
|
+
const { ImapFlow } = await import("imapflow");
|
|
8272
|
+
const { simpleParser } = await import("mailparser");
|
|
8273
|
+
const client = new ImapFlow({
|
|
8274
|
+
host: config.host,
|
|
8275
|
+
port: config.port,
|
|
8276
|
+
secure: config.secure,
|
|
8277
|
+
auth: { user: config.username, pass: password },
|
|
8278
|
+
logger: false
|
|
8279
|
+
});
|
|
8280
|
+
try {
|
|
8281
|
+
await client.connect();
|
|
8282
|
+
} catch (error) {
|
|
8283
|
+
const text = String(error.responseText ?? error.message ?? error);
|
|
8284
|
+
throw new HelperFailure(/auth|credential|login|password/i.test(text) ? `the mailbox refused the username or password \u2014 for Gmail and Outlook this has to be an app password (${text.slice(0, 120)})` : `could not reach the mailbox at ${config.host}:${config.port} \u2014 ${text.slice(0, 160)}`);
|
|
8285
|
+
}
|
|
8286
|
+
return {
|
|
8287
|
+
async latest(query, limit) {
|
|
8288
|
+
const lock = await client.getMailboxLock(config.folder);
|
|
8289
|
+
try {
|
|
8290
|
+
const criteria = {};
|
|
8291
|
+
if (query.to) criteria["to"] = query.to;
|
|
8292
|
+
if (query.from) criteria["from"] = query.from;
|
|
8293
|
+
if (query.subject) criteria["subject"] = query.subject;
|
|
8294
|
+
if (query.since) criteria["since"] = new Date(query.since.getTime() - 864e5);
|
|
8295
|
+
const found = await client.search(Object.keys(criteria).length ? criteria : { all: true }, { uid: true });
|
|
8296
|
+
const uids = (Array.isArray(found) ? found : []).slice(-limit).reverse();
|
|
8297
|
+
const out = [];
|
|
8298
|
+
for (const uid of uids) {
|
|
8299
|
+
const message = await client.fetchOne(String(uid), { source: true, internalDate: true }, { uid: true });
|
|
8300
|
+
if (!message || !message.source) continue;
|
|
8301
|
+
const parsed = await simpleParser(message.source);
|
|
8302
|
+
const addresses = (v) => {
|
|
8303
|
+
const list = Array.isArray(v) ? v : v ? [v] : [];
|
|
8304
|
+
return list.map((a) => a.text ?? "").join(", ");
|
|
8305
|
+
};
|
|
8306
|
+
out.push({
|
|
8307
|
+
received: message.internalDate ? new Date(message.internalDate) : parsed.date ?? /* @__PURE__ */ new Date(0),
|
|
8308
|
+
from: addresses(parsed.from),
|
|
8309
|
+
to: addresses(parsed.to),
|
|
8310
|
+
subject: parsed.subject ?? "",
|
|
8311
|
+
text: parsed.text ?? "",
|
|
8312
|
+
html: typeof parsed.html === "string" ? parsed.html : ""
|
|
8313
|
+
});
|
|
8314
|
+
}
|
|
8315
|
+
return out;
|
|
8316
|
+
} finally {
|
|
8317
|
+
lock.release();
|
|
8318
|
+
}
|
|
8319
|
+
},
|
|
8320
|
+
async close() {
|
|
8321
|
+
await client.logout().catch(() => void 0);
|
|
8322
|
+
}
|
|
8323
|
+
};
|
|
8324
|
+
}
|
|
8325
|
+
async function runMailbox(config, ctx) {
|
|
8326
|
+
const now = ctx.now ?? Date.now;
|
|
8327
|
+
const query = {
|
|
8328
|
+
to: fill(config.match.to, ctx.inputs),
|
|
8329
|
+
from: fill(config.match.from, ctx.inputs),
|
|
8330
|
+
subject: fill(config.match.subject, ctx.inputs),
|
|
8331
|
+
since: ctx.since ?? null
|
|
8332
|
+
};
|
|
8333
|
+
const mailbox = await (ctx.openMailbox ?? imapMailbox)(config, secret(ctx, config.passwordRef, "mailbox password"));
|
|
8334
|
+
const deadline = now() + config.waitSeconds * 1e3;
|
|
8335
|
+
try {
|
|
8336
|
+
for (; ; ) {
|
|
8337
|
+
const messages = (await mailbox.latest(query, 20)).filter((m) => !query.since || m.received.getTime() >= query.since.getTime()).sort((a, b2) => b2.received.getTime() - a.received.getTime());
|
|
8338
|
+
const message = messages[0];
|
|
8339
|
+
if (message) {
|
|
8340
|
+
const evidence = { from: message.from, subject: message.subject, received: ago(now(), message.received) };
|
|
8341
|
+
const body = message.text || textOfHtml(message.html);
|
|
8342
|
+
switch (config.extract.take) {
|
|
8343
|
+
case "subject":
|
|
8344
|
+
return { value: message.subject, detail: `the subject of "${message.subject}"`, evidence };
|
|
8345
|
+
case "body":
|
|
8346
|
+
return { value: body.slice(0, 4e3), detail: `the text of "${message.subject}"`, evidence };
|
|
8347
|
+
case "link": {
|
|
8348
|
+
const links = linksIn(message.html, message.text).filter((url) => !config.extract.pattern || url.includes(config.extract.pattern));
|
|
8349
|
+
if (!links[0]) throw new HelperFailure(`"${message.subject}" has no link${config.extract.pattern ? ` containing "${config.extract.pattern}"` : ""}`);
|
|
8350
|
+
return { value: links[0], detail: `the link in "${message.subject}"`, evidence };
|
|
8351
|
+
}
|
|
8352
|
+
case "code": {
|
|
8353
|
+
const code = takeCode(`${message.subject}
|
|
8354
|
+
${body}`, config.extract.pattern);
|
|
8355
|
+
if (!code) throw new HelperFailure(`"${message.subject}" has nothing matching ${config.extract.pattern || "a 4\u20138 digit code"}`);
|
|
8356
|
+
return { value: code, detail: `the code in "${message.subject}"`, evidence };
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8360
|
+
if (now() >= deadline || ctx.signal?.aborted) {
|
|
8361
|
+
const looked = [query.to && `to ${query.to}`, query.from && `from ${query.from}`, query.subject && `about "${query.subject}"`].filter(Boolean).join(", ");
|
|
8362
|
+
throw new HelperFailure(`no email${looked ? ` ${looked}` : ""}${query.since ? " since this started" : ""} arrived within ${config.waitSeconds}s`);
|
|
8363
|
+
}
|
|
8364
|
+
await new Promise((r) => setTimeout(r, ctx.pollMs ?? 3e3));
|
|
8365
|
+
}
|
|
8366
|
+
} finally {
|
|
8367
|
+
await mailbox.close();
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
async function runSms(config, ctx) {
|
|
8371
|
+
const now = ctx.now ?? Date.now;
|
|
8372
|
+
const http = ctx.fetch ?? fetch;
|
|
8373
|
+
const to = fill(config.to, ctx.inputs);
|
|
8374
|
+
const from = fill(config.match.from, ctx.inputs);
|
|
8375
|
+
const contains = fill(config.match.contains, ctx.inputs);
|
|
8376
|
+
const token = secret(ctx, config.authTokenRef, "Twilio auth token");
|
|
8377
|
+
const auth = Buffer.from(`${config.accountSid}:${token}`).toString("base64");
|
|
8378
|
+
const deadline = now() + config.waitSeconds * 1e3;
|
|
8379
|
+
for (; ; ) {
|
|
8380
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(config.accountSid)}/Messages.json?To=${encodeURIComponent(to)}&PageSize=20`;
|
|
8381
|
+
const response = await http(url, { headers: { authorization: `Basic ${auth}` }, ...ctx.signal ? { signal: ctx.signal } : {} });
|
|
8382
|
+
if (response.status === 401) throw new HelperFailure("Twilio refused the account SID or auth token");
|
|
8383
|
+
if (!response.ok) throw new HelperFailure(`Twilio answered ${response.status}`);
|
|
8384
|
+
const body = await response.json();
|
|
8385
|
+
const messages = (body.messages ?? []).map((m) => ({ ...m, at: new Date(m.date_sent ?? m.date_created) })).filter((m) => (!from || m.from.includes(from)) && (!contains || m.body.includes(contains))).filter((m) => !ctx.since || m.at.getTime() >= ctx.since.getTime()).sort((a, b2) => b2.at.getTime() - a.at.getTime());
|
|
8386
|
+
const message = messages[0];
|
|
8387
|
+
if (message) {
|
|
8388
|
+
const code = takeCode(message.body, config.extract.pattern);
|
|
8389
|
+
const evidence = { from: message.from, text: message.body.slice(0, 160), received: ago(now(), message.at) };
|
|
8390
|
+
if (!code) throw new HelperFailure(`the latest text has nothing matching ${config.extract.pattern || "a 4\u20138 digit code"}`);
|
|
8391
|
+
return { value: code, detail: `the code in the text from ${message.from}`, evidence };
|
|
8392
|
+
}
|
|
8393
|
+
if (now() >= deadline || ctx.signal?.aborted) {
|
|
8394
|
+
throw new HelperFailure(`no text to ${to}${ctx.since ? " since this started" : ""} arrived within ${config.waitSeconds}s`);
|
|
8395
|
+
}
|
|
8396
|
+
await new Promise((r) => setTimeout(r, ctx.pollMs ?? 3e3));
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8399
|
+
function atPath(value, path11) {
|
|
8400
|
+
let at = value;
|
|
8401
|
+
for (const key of path11.split(".").filter(Boolean)) {
|
|
8402
|
+
if (at === null || at === void 0) return void 0;
|
|
8403
|
+
at = Array.isArray(at) && /^\d+$/.test(key) ? at[Number(key)] : at[key];
|
|
8404
|
+
}
|
|
8405
|
+
return at;
|
|
8406
|
+
}
|
|
8407
|
+
async function runHttp(config, ctx) {
|
|
8408
|
+
const http = ctx.fetch ?? fetch;
|
|
8409
|
+
const url = fill(config.url, ctx.inputs);
|
|
8410
|
+
try {
|
|
8411
|
+
new URL(url);
|
|
8412
|
+
} catch {
|
|
8413
|
+
throw new HelperFailure(`"${url}" is not an address`);
|
|
8414
|
+
}
|
|
8415
|
+
const headers = {};
|
|
8416
|
+
for (const [name, value] of Object.entries(config.headers)) headers[name] = fill(value, ctx.inputs);
|
|
8417
|
+
for (const [name, ref] of Object.entries(config.secretHeaders)) headers[name] = secret(ctx, ref, `"${name}" header`);
|
|
8418
|
+
const body = config.method === "GET" || !config.body ? void 0 : fill(config.body, ctx.inputs);
|
|
8419
|
+
if (body && !Object.keys(headers).some((h) => h.toLowerCase() === "content-type")) {
|
|
8420
|
+
headers["content-type"] = /^\s*[[{]/.test(body) ? "application/json" : "text/plain";
|
|
8421
|
+
}
|
|
8422
|
+
const timeout = AbortSignal.timeout(3e4);
|
|
8423
|
+
let response;
|
|
8424
|
+
try {
|
|
8425
|
+
response = await http(url, { method: config.method, headers, ...body !== void 0 ? { body } : {}, signal: ctx.signal ? AbortSignal.any([ctx.signal, timeout]) : timeout });
|
|
8426
|
+
} catch (error) {
|
|
8427
|
+
throw new HelperFailure(`could not reach ${new URL(url).host} \u2014 ${error.message}`);
|
|
8428
|
+
}
|
|
8429
|
+
const text = await response.text();
|
|
8430
|
+
const ok = config.expectStatus ? response.status === config.expectStatus : response.ok;
|
|
8431
|
+
if (!ok) throw new HelperFailure(`${config.method} ${new URL(url).pathname} answered ${response.status}${config.expectStatus ? `, not ${config.expectStatus}` : ""}: ${text.slice(0, 200)}`);
|
|
8432
|
+
const evidence = { status: String(response.status), response: text.slice(0, 300) };
|
|
8433
|
+
switch (config.extract.take) {
|
|
8434
|
+
case "nothing":
|
|
8435
|
+
return { value: "", detail: `${config.method} ${new URL(url).pathname} answered ${response.status}`, evidence };
|
|
8436
|
+
case "status":
|
|
8437
|
+
return { value: String(response.status), detail: "the response status", evidence };
|
|
8438
|
+
case "pattern": {
|
|
8439
|
+
const found = takeCode(text, config.extract.pattern);
|
|
8440
|
+
if (found === null) throw new HelperFailure(`the response has nothing matching ${config.extract.pattern}`);
|
|
8441
|
+
return { value: found, detail: "matched in the response", evidence };
|
|
8442
|
+
}
|
|
8443
|
+
case "json": {
|
|
8444
|
+
let parsed;
|
|
8445
|
+
try {
|
|
8446
|
+
parsed = JSON.parse(text);
|
|
8447
|
+
} catch {
|
|
8448
|
+
throw new HelperFailure("the response is not JSON");
|
|
8449
|
+
}
|
|
8450
|
+
const value = atPath(parsed, config.extract.path);
|
|
8451
|
+
if (value === void 0 || value === null) throw new HelperFailure(`the response has no "${config.extract.path}"`);
|
|
8452
|
+
return { value: typeof value === "string" ? value : JSON.stringify(value), detail: `"${config.extract.path}" from the response`, evidence };
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
}
|
|
8456
|
+
async function runHelper(helper, ctx) {
|
|
8457
|
+
const config = "config" in helper ? helper.config : helper;
|
|
8458
|
+
switch (config.kind) {
|
|
8459
|
+
case "totp": {
|
|
8460
|
+
const { code, remaining } = totp(secret(ctx, config.secretRef, "authenticator secret"), (ctx.now ?? Date.now)(), config);
|
|
8461
|
+
return { value: code, detail: `valid for ${remaining} more seconds`, evidence: {} };
|
|
8462
|
+
}
|
|
8463
|
+
case "mailbox":
|
|
8464
|
+
return runMailbox(config, ctx);
|
|
8465
|
+
case "sms":
|
|
8466
|
+
return runSms(config, ctx);
|
|
8467
|
+
case "http":
|
|
8468
|
+
return runHttp(config, ctx);
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8019
8472
|
// ../execution/dist/index.js
|
|
8020
8473
|
import fs24 from "fs";
|
|
8021
8474
|
import crypto3 from "crypto";
|
|
@@ -8091,6 +8544,7 @@ async function executeFlow(opts) {
|
|
|
8091
8544
|
...flow.actors ? { actors: flow.actors } : {},
|
|
8092
8545
|
...flow.defaultActorId ? { defaultActorId: flow.defaultActorId } : {},
|
|
8093
8546
|
...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
|
|
8547
|
+
...store.runHelper ? { runHelper: store.runHelper } : {},
|
|
8094
8548
|
...opts.reuse ? { reuse: opts.reuse } : {},
|
|
8095
8549
|
// The flow's own origin when it has one, the project's otherwise. A flow
|
|
8096
8550
|
// that carries no origin is the normal case now: the environment belongs
|
|
@@ -8311,6 +8765,28 @@ function toStepRow(ordinal, result, step) {
|
|
|
8311
8765
|
checks
|
|
8312
8766
|
};
|
|
8313
8767
|
}
|
|
8768
|
+
var CLOCK_SLACK_MS = 1e4;
|
|
8769
|
+
function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date()) {
|
|
8770
|
+
if (!Array.isArray(delivered) || delivered.length === 0) return void 0;
|
|
8771
|
+
const helpers = new Map(
|
|
8772
|
+
DeliveredHelperSchema.array().parse(delivered).map((helper) => [helper.id, helper])
|
|
8773
|
+
);
|
|
8774
|
+
const since = new Date(startedAt.getTime() - CLOCK_SLACK_MS);
|
|
8775
|
+
return async (helperId, inputs) => {
|
|
8776
|
+
const helper = helpers.get(helperId);
|
|
8777
|
+
if (!helper) throw new Error("that helper was deleted, or was not sent with this run \u2014 choose another");
|
|
8778
|
+
try {
|
|
8779
|
+
const { value, detail } = await runHelper(helper.config, {
|
|
8780
|
+
resolveSecret: (ref) => secrets?.[ref],
|
|
8781
|
+
inputs,
|
|
8782
|
+
since
|
|
8783
|
+
});
|
|
8784
|
+
return { value, detail: `${helper.name}: ${detail}` };
|
|
8785
|
+
} catch (error) {
|
|
8786
|
+
throw new Error(`${helper.name}: ${error.message}`);
|
|
8787
|
+
}
|
|
8788
|
+
};
|
|
8789
|
+
}
|
|
8314
8790
|
var LOCAL_CHROMIUM2 = { id: "chromium", provider: "local", engine: "chromium" };
|
|
8315
8791
|
async function summaryOf2(storage, runId) {
|
|
8316
8792
|
const detail = await storage.getRun(runId);
|
|
@@ -8371,6 +8847,7 @@ async function executeCase(opts) {
|
|
|
8371
8847
|
const stepsByOccurrence = new Map(testCase.occurrences.map((o) => [o.occurrenceId, new Map(o.steps.map((s2) => [s2.id, s2]))]));
|
|
8372
8848
|
const ordinals = /* @__PURE__ */ new Map();
|
|
8373
8849
|
const writes = [];
|
|
8850
|
+
const helpers = helperRunner(opts.helpers, opts.secrets);
|
|
8374
8851
|
let outcome;
|
|
8375
8852
|
try {
|
|
8376
8853
|
outcome = await replayCase({
|
|
@@ -8378,6 +8855,7 @@ async function executeCase(opts) {
|
|
|
8378
8855
|
...testCase.actors ? { actors: testCase.actors } : {},
|
|
8379
8856
|
...testCase.defaultActorId ? { defaultActorId: testCase.defaultActorId } : {},
|
|
8380
8857
|
resolveAuth: authenticationResolver(opts.authentications),
|
|
8858
|
+
...helpers ? { runHelper: helpers } : {},
|
|
8381
8859
|
...opts.reuse ? { reuse: opts.reuse } : {},
|
|
8382
8860
|
baseUrl: testCase.baseUrl ?? baseUrl,
|
|
8383
8861
|
...testCase.environment ? { environment: testCase.environment } : {},
|
|
@@ -8572,6 +9050,7 @@ var RecordingCoordinator = class {
|
|
|
8572
9050
|
/** Everything that used to happen before `start` answered. */
|
|
8573
9051
|
async open(target, opts) {
|
|
8574
9052
|
const config = this.deps.store.readConfig();
|
|
9053
|
+
const helpers = helperRunner(opts.helpers, opts.secrets);
|
|
8575
9054
|
const configuredBase = config.baseUrl;
|
|
8576
9055
|
try {
|
|
8577
9056
|
this.session = await startRecordingSession({
|
|
@@ -8579,12 +9058,14 @@ var RecordingCoordinator = class {
|
|
|
8579
9058
|
...opts.authentication ? { authentication: {
|
|
8580
9059
|
step: { id: "auth-preparation", type: "signIn", auth: opts.authentication, name: "Sign in before recording", role: "", scope: "", value: "", secret: false, legacySelector: "", proposals: [] },
|
|
8581
9060
|
resolveAuth: authenticationResolver(opts.authentications),
|
|
8582
|
-
resolveSecret: (ref) => opts.secrets?.[ref]
|
|
9061
|
+
resolveSecret: (ref) => opts.secrets?.[ref],
|
|
9062
|
+
...helpers ? { runHelper: helpers } : {}
|
|
8583
9063
|
} } : {},
|
|
8584
9064
|
...opts.preparation?.length ? { preparation: {
|
|
8585
9065
|
steps: opts.preparation,
|
|
8586
9066
|
resolveAuth: authenticationResolver(opts.authentications),
|
|
8587
|
-
resolveSecret: (ref) => opts.secrets?.[ref]
|
|
9067
|
+
resolveSecret: (ref) => opts.secrets?.[ref],
|
|
9068
|
+
...helpers ? { runHelper: helpers } : {}
|
|
8588
9069
|
} } : {},
|
|
8589
9070
|
// The project's saved defaults, unless this recording asks for
|
|
8590
9071
|
// something else. Without the fallback the settings screen would write
|
|
@@ -8778,6 +9259,7 @@ var DebugCoordinator = class {
|
|
|
8778
9259
|
...flow.actors ? { actors: flow.actors } : {},
|
|
8779
9260
|
...flow.defaultActorId ? { defaultActorId: flow.defaultActorId } : {},
|
|
8780
9261
|
...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
|
|
9262
|
+
...store.runHelper ? { runHelper: store.runHelper } : {},
|
|
8781
9263
|
environment: store.environmentFor(flow),
|
|
8782
9264
|
resolveFile: (name) => store.resolveFile(name),
|
|
8783
9265
|
baseUrl,
|
|
@@ -8911,6 +9393,7 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8911
9393
|
const secrets = store.readSecrets();
|
|
8912
9394
|
return await verifySteps({
|
|
8913
9395
|
...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
|
|
9396
|
+
...store.runHelper ? { runHelper: store.runHelper } : {},
|
|
8914
9397
|
environment: store.environmentFor(flow),
|
|
8915
9398
|
resolveFile: (name) => store.resolveFile(name),
|
|
8916
9399
|
// Streamed, so the editor fills in as the pass proceeds instead of
|
|
@@ -8935,8 +9418,10 @@ async function verifyFlowSteps(deps, opts) {
|
|
|
8935
9418
|
}
|
|
8936
9419
|
function deliveredRun(delivered) {
|
|
8937
9420
|
const { flow, secrets, config } = delivered;
|
|
9421
|
+
const runHelper2 = helperRunner(delivered.helpers, secrets);
|
|
8938
9422
|
return {
|
|
8939
9423
|
resolveAuth: authenticationResolver(delivered.authentications),
|
|
9424
|
+
...runHelper2 ? { runHelper: runHelper2 } : {},
|
|
8940
9425
|
readConfig: () => config,
|
|
8941
9426
|
// By id, and only the one that was sent. A machine serving no checkout has
|
|
8942
9427
|
// no other flow to confuse it with.
|
|
@@ -9030,7 +9515,7 @@ function resolveCheckouts(dirs) {
|
|
|
9030
9515
|
return { projects, skipped };
|
|
9031
9516
|
}
|
|
9032
9517
|
|
|
9033
|
-
// ../agent-core/dist/chunk-
|
|
9518
|
+
// ../agent-core/dist/chunk-LR7LBGCT.js
|
|
9034
9519
|
import { WebSocket, WebSocketServer } from "ws";
|
|
9035
9520
|
var HEARTBEAT_MS = 3e4;
|
|
9036
9521
|
function adapt(socket) {
|
|
@@ -9917,6 +10402,7 @@ function executionHandlers(ctx) {
|
|
|
9917
10402
|
const source = carried ? deliveredRun({
|
|
9918
10403
|
flow: carried,
|
|
9919
10404
|
authentications: command.args["authentications"],
|
|
10405
|
+
helpers: command.args["helpers"],
|
|
9920
10406
|
secrets: command.args["secrets"] ?? {},
|
|
9921
10407
|
config: command.args["config"] ?? {}
|
|
9922
10408
|
}) : storeFor(project.root);
|
|
@@ -10023,6 +10509,7 @@ function executionHandlers(ctx) {
|
|
|
10023
10509
|
const { outcome, run } = await executeCase({
|
|
10024
10510
|
testCase,
|
|
10025
10511
|
authentications: command.args["authentications"],
|
|
10512
|
+
helpers: command.args["helpers"],
|
|
10026
10513
|
projectId: project.id,
|
|
10027
10514
|
config: command.args["config"] ?? {},
|
|
10028
10515
|
secrets: command.args["secrets"] ?? {},
|
|
@@ -10105,6 +10592,7 @@ function recordingHandlers({ recorder, onActivity }) {
|
|
|
10105
10592
|
...Array.isArray(command.args["preparation"]) && command.args["preparation"].length > 0 ? { preparation: RecordedStepSchema.array().parse(command.args["preparation"]) } : {},
|
|
10106
10593
|
...command.args["authentication"] || command.args["preparation"] ? {
|
|
10107
10594
|
authentications: command.args["authentications"],
|
|
10595
|
+
helpers: command.args["helpers"],
|
|
10108
10596
|
secrets: command.args["secrets"] ?? {}
|
|
10109
10597
|
} : {},
|
|
10110
10598
|
projectId: project.id,
|
|
@@ -10191,6 +10679,7 @@ function debuggingHandlers(ctx) {
|
|
|
10191
10679
|
...carried ? { delivered: {
|
|
10192
10680
|
flow: carried,
|
|
10193
10681
|
authentications: command.args["authentications"],
|
|
10682
|
+
helpers: command.args["helpers"],
|
|
10194
10683
|
secrets: command.args["secrets"] ?? {},
|
|
10195
10684
|
config: command.args["config"] ?? {}
|
|
10196
10685
|
} } : {},
|
|
@@ -10232,6 +10721,7 @@ function debuggingHandlers(ctx) {
|
|
|
10232
10721
|
delivered: {
|
|
10233
10722
|
flow: carried,
|
|
10234
10723
|
authentications: command.args["authentications"],
|
|
10724
|
+
helpers: command.args["helpers"],
|
|
10235
10725
|
secrets: command.args["secrets"] ?? {},
|
|
10236
10726
|
config: command.args["config"] ?? {},
|
|
10237
10727
|
scratchRoot: scratchRoot(project.id)
|
|
@@ -10362,6 +10852,7 @@ function createAgentRuntime(options) {
|
|
|
10362
10852
|
store: deliveredRun({
|
|
10363
10853
|
flow: delivered.flow,
|
|
10364
10854
|
authentications: delivered.authentications,
|
|
10855
|
+
helpers: delivered.helpers,
|
|
10365
10856
|
secrets: delivered.secrets,
|
|
10366
10857
|
config: delivered.config
|
|
10367
10858
|
}),
|
package/dist/cli.js
CHANGED
package/dist/engine.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowriki/agent",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.28",
|
|
4
4
|
"description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"arm64"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"imapflow": "^1.7.8",
|
|
34
|
+
"mailparser": "^3.9.0",
|
|
33
35
|
"playwright": "1.63.0",
|
|
34
36
|
"ws": "^8.21.3",
|
|
35
37
|
"zod": "^3.24.1"
|