@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.
@@ -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.26";
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 = (secret) => crypto2.createHash("sha256").update(secret).digest("hex").slice(0, 12);
136
+ var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
137
137
 
138
- // ../contracts/dist/chunk-MQTBBCDK.js
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-TSFRTYLG.js
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, { name: plan.name, exact: true, includeHidden: opts.includeHidden });
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, secret) => text.split(secret).join("[secret]"), line);
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, opts.actors?.values ?? /* @__PURE__ */ new Map());
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 step = options.valuesResolved ? parsed : withRuntimeValues(parsed, options.actors?.values ?? /* @__PURE__ */ new Map());
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-A4JZPKZT.js
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(/&amp;/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(/&nbsp;/g, " ").replace(/&amp;/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-4MOK25KG.js
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
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-IH7KFCV4.js";
11
+ } from "./chunk-PETFNOJA.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { parseArgs } from "util";
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-IH7KFCV4.js";
12
+ } from "./chunk-PETFNOJA.js";
13
13
  export {
14
14
  AGENT_VERSION,
15
15
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.26",
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"