blastproof 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,7 +113,7 @@ jobs:
113
113
 
114
114
  - run: npm start & # however your app boots
115
115
 
116
- - uses: hamc/blastproof@v0.7.0
116
+ - uses: hamc/blastproof@v0.8.0
117
117
  with:
118
118
  version: '0.6.0' # pin both when this gates merges
119
119
  api-key: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -241,7 +241,9 @@ The key itself is never read from a `BLASTPROOF_*` variable — you name *which*
241
241
 
242
242
  The application under test is not trusted input: its page content reaches the model, so a page that controls its own accessible text can try to influence the agent. Two things constrain that.
243
243
 
244
- **The agent cannot leave your application.** `navigate` is bounded by `base_url`'s origin; an app spanning hosts declares them in `allowed_origins:`. This is enforced by comparison, not by asking the model nicely.
244
+ **The agent cannot leave your application.** The boundary is `base_url`'s origin plus whatever `allowed_origins:` declares, and it constrains where the page **is**, not only where an action asked to go. A `navigate` outside it is refused before the request; a page that ends up outside it any other way — a redirect, a link to another host, a script setting the location — fails the step, and its content is never sent to the model. Enforced by comparison, not by asking the model nicely.
245
+
246
+ If your application legitimately spans hosts (an identity provider, a hosted payment step), declare them. A suite that was quietly walking onto a foreign page will now fail and name the origin to add.
245
247
 
246
248
  **Your secrets stay out of prompts.** `{{env.*}}` placeholders survive intact and are substituted at the moment of typing. Every value your tests or auth recipe reference is redacted from everything else crossing into a prompt — page snapshots included — in literal and percent-encoded form. Redaction matches known values, so treat it as a strong default rather than a guarantee against a hostile app.
247
249
 
package/dist/cli.js CHANGED
@@ -336,14 +336,33 @@ var ActionError = class extends Error {
336
336
  this.name = "ActionError";
337
337
  }
338
338
  };
339
- function assertAllowedOrigin(url, ctx) {
340
- const allowed = /* @__PURE__ */ new Set([new URL(ctx.baseUrl).origin]);
341
- for (const origin of ctx.allowedOrigins ?? []) {
339
+ var BLANK_PAGE = "about:blank";
340
+ function allowedOriginsFor(baseUrl, allowedOrigins) {
341
+ const allowed = /* @__PURE__ */ new Set([new URL(baseUrl).origin]);
342
+ for (const origin of allowedOrigins ?? []) {
342
343
  allowed.add(new URL(origin).origin);
343
344
  }
344
- if (!allowed.has(url.origin)) {
345
+ return allowed;
346
+ }
347
+ function isOriginAllowed(url, allowed) {
348
+ if (url === BLANK_PAGE) return true;
349
+ let origin;
350
+ try {
351
+ origin = new URL(url).origin;
352
+ } catch {
353
+ return false;
354
+ }
355
+ if (origin === "null") return false;
356
+ return allowed.has(origin);
357
+ }
358
+ function describeBoundary(allowed) {
359
+ return [...allowed].join(" or ");
360
+ }
361
+ function assertAllowedOrigin(url, ctx) {
362
+ const allowed = allowedOriginsFor(ctx.baseUrl, ctx.allowedOrigins);
363
+ if (!isOriginAllowed(url.toString(), allowed)) {
345
364
  throw new ActionError(
346
- `Refusing to navigate outside the application: ${url.origin} is not ${[...allowed].join(" or ")}. Add it to allowed_origins in .blastproof/config.yaml if the app legitimately spans hosts.`
365
+ `Refusing to navigate outside the application: ${url.origin} is not ${describeBoundary(allowed)}. Add it to allowed_origins in .blastproof/config.yaml if the app legitimately spans hosts.`
347
366
  );
348
367
  }
349
368
  }
@@ -548,6 +567,7 @@ async function executeTest(page, test, options) {
548
567
  onEvent({ type: "action", index, action: maskedAction, result: mask(result) });
549
568
  };
550
569
  let failure;
570
+ const boundary = allowedOriginsFor(baseUrl, allowedOrigins);
551
571
  await page.goto(new URL(baseUrl).toString());
552
572
  for (let index = 0; index < allSteps.length; index++) {
553
573
  const { step, setup } = allSteps[index];
@@ -564,6 +584,11 @@ async function executeTest(page, test, options) {
564
584
  throw new StepFailure(`step exceeded ${maxIterationsPerStep} actions without completing`);
565
585
  }
566
586
  await waitForSettled(page);
587
+ if (!isOriginAllowed(page.url(), boundary)) {
588
+ throw new StepFailure(
589
+ `The page left the application: ${page.url()} is outside ${describeBoundary(boundary)}. Add it to allowed_origins in .blastproof/config.yaml if the app legitimately spans hosts.`
590
+ );
591
+ }
567
592
  const snap = await takeSnapshot(page);
568
593
  let action;
569
594
  try {
@@ -2635,7 +2660,7 @@ function parsePositiveNumber(flag) {
2635
2660
  };
2636
2661
  }
2637
2662
  var program = new Command();
2638
- program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.7.0");
2663
+ program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.8.0");
2639
2664
  program.command("init").description("Scaffold .blastproof/ (config, tests, sample tests) in the current directory").action(async () => {
2640
2665
  try {
2641
2666
  const result = await initProject(process.cwd());