DEV Community

Daniel Pertu
Daniel Pertu

Posted on

The guard that stops our automation from following a link

Our app can reply to a rental listing for you by replaying a form-filling recipe you recorded once. The most important code in that feature is not the part that fills forms. It is the part that refuses to.

Here is the scenario it exists for. You are on a listing on some aggregator. You click "Contact landlord". The aggregator does not have a contact form — it hands you off to a completely different company's site, which asks you to register an account, verify an email, and pay €29.99 for a "premium membership" before you may send a message.

A human notices the handoff instantly. An automation that is just replaying "click the thing, fill the fields, press submit" does not notice anything at all. It will cheerfully proceed to create an account in your name on a site you have never heard of, and the only question is how far it gets before something fails.

So the first guard is a hard domain lock.

eTLD+1, not hostname

You cannot compare hostnames. www.example.com and example.com and m.example.com are the same site; a.somehost.io and b.somehost.io very often are not. The correct unit is the registrable domain — the eTLD+1 — and you cannot compute that with string operations, because whether .co.uk or .com.au is a public suffix is a fact about the world, maintained in a list.

import { getDomain } from 'tldts';

/**
 * Registrable domain (eTLD+1) for a URL, e.g.
 *   https://www.pararius.nl/x  -> pararius.nl
 *   https://a.example.co.uk/y  -> example.co.uk
 *
 * `allowPrivateDomains` is on deliberately: it treats `a.someplatform.io` and
 * `b.someplatform.io` as different sites, which is the strict (safe) direction
 * for a guard.
 */
export function registrableDomain(url: string): string | null {
  try {
    return getDomain(url, { allowPrivateDomains: true }) ?? null;
  } catch {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

allowPrivateDomains is the subtle flag. With it on, the Public Suffix List's private section is honoured, so github.io, vercel.app, herokuapp.com and friends are treated as suffixes — meaning two tenants on the same hosting platform read as two different sites rather than one.

For most purposes that is too strict. For a guard it is exactly right, and the general principle is worth naming: when a helper is used to decide whether something is allowed, every ambiguous case should resolve towards "not allowed". Here, over-splitting produces a refusal. Under-splitting produces a message sent through a third party.

Note also that a parse failure returns null rather than throwing, and null never matches anything, so a malformed URL fails closed too.

Recording is guarded as well as replay

The obvious place to check is replay. The less obvious one is the recorder, and it is just as important:

ipcMain.on('recorder:step', (_event, step: CapturedStep) => {
  if (!_session) return;

  // Never record an action taken on another company's site. This is the
  // aggregator hand-off case, and it is the one thing the replayer must never
  // learn how to do.
  if (!sameSite(_session.domain, step.url)) {
    _session.wentOffsite = true;
    return;
  }
  _session.steps.push(step);
});
Enter fullscreen mode Exit fullscreen mode

If the user themselves clicks through to the third-party site during recording — which they might, because they are a person doing a task, not a test fixture — those steps are dropped and the session is flagged. The alternative is a recipe that contains a signup flow on a foreign domain, and a replay-side guard that has to catch it every single time thereafter. Better to never write it down.

Guard at the point data enters the system, not only at the point it is used. A recorded artifact is a persisted decision, and persisting a dangerous one and relying on downstream checks means one missed check turns into a repeatable incident.

The rest of the list

The domain lock is one of seven:

export type SafetyBlock =
  | 'offsite'
  | 'payment'
  | 'captcha'
  | 'login'
  | 'signup'
  | 'paywall'
  | 'not_listing';
Enter fullscreen mode Exit fullscreen mode

Each is a page condition that stops the replay before anything is typed or clicked, and each is recorded on the reply record so the user is told which guard tripped rather than "something went wrong". The header on the module says the important part:

 * Every rule here is deterministic and runs before anything is typed or
 * clicked. None of it is delegated to a model — the whole point is that the
 * refusals are predictable and auditable.
Enter fullscreen mode Exit fullscreen mode

There is a real argument for a model here — it would catch weird handoffs a rule list misses. We did not take it, because a guard that is right 97% of the time is not a guard. Its whole job is the tail.

Try to trip it

The guards ship in the app: notifio.app. Record a reply flow against a form you control, then add a link that bounces to a different domain mid-flow and replay it. You should get offsite and nothing submitted.

If you would rather see which sites do the aggregator handoff in the wild before installing anything, the per-site notes at notifio.app/alerts cover which ones keep you on-platform and which sell you to someone else at the contact step.

Top comments (0)