Revamp login guards into general per-firm page guards
Replace the single hardcoded signed-out check with a guards list on each firm: any guard can trip mid-run via a URL substring (checked on every step) or a selector probe (checked when a step fails). Each guard defines recovery steps or a human-wait fallback, a per-run trigger cap, and a resume policy — 'continue' retries the tripped step, 'restart' rewinds the run to step 1. Runner bumped to 0.20.0; the server still ships the legacy signedOut/authSteps shape for stale runners, and a new runner synthesizes a guard from a legacy payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
82b98b9d2d
commit
b1dccd7dec
+122
-44
@@ -110,7 +110,11 @@ export interface Automation {
|
||||
}
|
||||
|
||||
/** A step as handed to the runner: the firm's tab pattern and fallback URL
|
||||
* filled in, so the runner never has to know which firm it is working on. */
|
||||
* filled in, so the runner never has to know which firm it is working on.
|
||||
*
|
||||
* `signedOut`/`signedOutWait` are the pre-guard (runner < 0.20) shape of the
|
||||
* login guard, still stamped on so a stale runner keeps working — a 0.20+
|
||||
* runner ignores them in favour of the run's `guards`. */
|
||||
export type ResolvedStep = AutomationStep & {
|
||||
urlPattern?: string;
|
||||
openUrl?: string;
|
||||
@@ -118,6 +122,59 @@ export type ResolvedStep = AutomationStep & {
|
||||
signedOutWait?: number;
|
||||
};
|
||||
|
||||
/** A condition that can take over the page at any point in a run — a login
|
||||
* redirect, a session-expired modal, a maintenance page, a rate-limit
|
||||
* interstitial. Guards belong to the firm, not to any step: once the page is
|
||||
* in that state, every selector resolves against the wrong document, so the
|
||||
* run must deal with the guard before anything else can proceed.
|
||||
*
|
||||
* How a guard trips:
|
||||
* - `urlContains` — the tab's URL carries this substring. Checked on every
|
||||
* step, for free: the URL rides along with every locate answer.
|
||||
* - `selector` — this element is on the page. A DOM probe costs a round
|
||||
* trip, so the runner checks these when a step fails, where a guard is
|
||||
* the likeliest explanation. A guard may define either or both (both =
|
||||
* tripped when either is present).
|
||||
*
|
||||
* What happens then:
|
||||
* 1. `recoverySteps` run, if there are any. They execute *while the guard
|
||||
* condition holds* — on the login form, under the modal — so they carry
|
||||
* no guard checks of their own; a wrong selector here is worse than
|
||||
* elsewhere. Anything written here lives in this file in plain text,
|
||||
* and this file is in the repo.
|
||||
* 2. With no recovery steps, the run pauses up to `waitSeconds` for a human
|
||||
* to deal with it (sign in, dismiss the thing). No recovery steps and no
|
||||
* wait means tripping the guard fails the run.
|
||||
* 3. Once the condition has verifiably cleared, `resume` decides where the
|
||||
* run picks up: `'continue'` retries the step that tripped and carries
|
||||
* on; `'restart'` goes back to the automation's first step — for
|
||||
* conditions that reset page state (a re-login that lands on the
|
||||
* dashboard, a modal that closes the checkout flow).
|
||||
*/
|
||||
export interface PageGuard {
|
||||
/** Stable id — names the guard in run logs and counts its triggers per run. */
|
||||
id: string;
|
||||
/** Shown in the run log; falls back to `id`. */
|
||||
label?: string;
|
||||
/** Trip when the tab's URL contains this substring. */
|
||||
urlContains?: string;
|
||||
/** Trip when this element is on the page. */
|
||||
selector?: string;
|
||||
/** Steps that clear the condition. Run guard-free, on the guard page itself. */
|
||||
recoverySteps?: AutomationStep[];
|
||||
/** Where the run picks up once the condition clears. */
|
||||
resume: 'restart' | 'continue';
|
||||
/** Seconds to pause for a human when there are no `recoverySteps`. */
|
||||
waitSeconds?: number;
|
||||
/** Times this guard may trip in one run before the run fails; a guard whose
|
||||
* recovery "works" without clearing the cause would otherwise loop.
|
||||
* Default 3. */
|
||||
maxTriggers?: number;
|
||||
}
|
||||
|
||||
/** A guard as handed to the runner: recovery steps expanded and resolved. */
|
||||
export type ResolvedGuard = Omit<PageGuard, 'recoverySteps'> & { recoverySteps: ResolvedStep[] };
|
||||
|
||||
export interface Firm {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -127,27 +184,11 @@ export interface Firm {
|
||||
/** Concrete page to open when no tab matches `urlPattern`. A match pattern
|
||||
* can't be navigated to, so this has to be spelled out separately. */
|
||||
url: string;
|
||||
/** Substring identifying the signed-out page. When a session expires the site
|
||||
* redirects here, and every subsequent selector resolves against a login form
|
||||
* — so a run that lands on it must stop rather than click through it. */
|
||||
signedOutPattern: string;
|
||||
/** Seconds to pause and let a human sign in when a run hits the login page.
|
||||
* 0 aborts instead. Used only when `authSteps` is empty. */
|
||||
signedOutWaitSeconds: number;
|
||||
/** Steps run when a run lands on the login page, before retrying the step
|
||||
* that hit it. Left empty here on purpose — fill it in yourself.
|
||||
*
|
||||
* Two things to know if you do:
|
||||
* - These run *while on the signed-out page*, so unlike normal steps they
|
||||
* carry no signed-out guard. Nothing stops them clicking around a login
|
||||
* form; that is the point, and also why a wrong selector here is worse
|
||||
* than elsewhere.
|
||||
* - Anything written here lives in this file in plain text, and this file
|
||||
* is in the repo.
|
||||
*
|
||||
* While empty, a run that hits the login page falls back to pausing for
|
||||
* `signedOutWaitSeconds` so you can sign in by hand. */
|
||||
authSteps: AutomationStep[];
|
||||
/** Conditions that can interrupt a run at any point, and how to get past
|
||||
* them. Every firm wants at least the signed-out guard: when a session
|
||||
* expires the site redirects to its login page, and a run that lands there
|
||||
* must stop rather than click through the form. See PageGuard. */
|
||||
guards: PageGuard[];
|
||||
automations: Automation[];
|
||||
}
|
||||
|
||||
@@ -160,10 +201,17 @@ export const FIRMS: Firm[] = [
|
||||
label: 'Tradeify',
|
||||
urlPattern: 'https://app-f.tradeify.co/*',
|
||||
url: 'https://app-f.tradeify.co/',
|
||||
signedOutPattern: '/auth/',
|
||||
signedOutWaitSeconds: 300,
|
||||
authSteps: [
|
||||
{ action: 'click', selector: 'form > div > div:last-child button', label: 'Login' },
|
||||
guards: [
|
||||
{
|
||||
id: 'signed-out',
|
||||
label: 'Signed out',
|
||||
urlContains: '/auth/',
|
||||
resume: 'continue',
|
||||
waitSeconds: 300,
|
||||
recoverySteps: [
|
||||
{ action: 'click', selector: 'form > div > div:last-child button', label: 'Login' },
|
||||
],
|
||||
},
|
||||
],
|
||||
automations: [
|
||||
{
|
||||
@@ -248,14 +296,20 @@ export const FIRMS: Firm[] = [
|
||||
// well as subdomains, confirmed against a live tab on lucidtrading.com.
|
||||
urlPattern: 'https://*.lucidtrading.com/*',
|
||||
url: 'https://dash.lucidtrading.com/',
|
||||
// Signed out lands on the apex; signed in stays on dash. The leading `//`
|
||||
// anchors this to the start of the host — without it, the substring also
|
||||
// matches dash.lucidtrading.com and every step would abort while logged in.
|
||||
signedOutPattern: '//lucidtrading.com/',
|
||||
signedOutWaitSeconds: 300,
|
||||
authSteps: [
|
||||
{ action: 'click', selector: 'button.lucid-login-btn', label: 'Login' },
|
||||
|
||||
guards: [
|
||||
{
|
||||
id: 'signed-out',
|
||||
label: 'Signed out',
|
||||
// Signed out lands on the apex; signed in stays on dash. The leading `//`
|
||||
// anchors this to the start of the host — without it, the substring also
|
||||
// matches dash.lucidtrading.com and every step would abort while logged in.
|
||||
urlContains: '//lucidtrading.com/',
|
||||
resume: 'continue',
|
||||
waitSeconds: 300,
|
||||
recoverySteps: [
|
||||
{ action: 'click', selector: 'button.lucid-login-btn', label: 'Login' },
|
||||
],
|
||||
},
|
||||
],
|
||||
automations: [
|
||||
{
|
||||
@@ -371,8 +425,17 @@ function labelled(step: ResolvedStep, iteration: number, total: number): Resolve
|
||||
return { ...step, label: `${step.label ?? describeStep(step)} (${iteration}/${total})` };
|
||||
}
|
||||
|
||||
/** The guard a pre-0.20 runner knew as "the" login guard: the first URL guard.
|
||||
* Its pattern is still stamped onto steps (`signedOut`) and its recovery steps
|
||||
* still ship as `authSteps`, so a stale runner keeps working until restarted.
|
||||
* A 0.20+ runner ignores both in favour of the run-level `guards`. */
|
||||
function legacyLoginGuard(firm: Firm): PageGuard | undefined {
|
||||
return firm.guards.find((g) => g.urlContains);
|
||||
}
|
||||
|
||||
/** One non-repeat step, with the firm's tab pattern and URLs filled in. */
|
||||
function resolveOne(firm: Firm, step: AutomationStep): ResolvedStep {
|
||||
const login = legacyLoginGuard(firm);
|
||||
if (step.action === 'wait') return step;
|
||||
if (step.action === 'navigate') {
|
||||
return {
|
||||
@@ -380,8 +443,8 @@ function resolveOne(firm: Firm, step: AutomationStep): ResolvedStep {
|
||||
url: step.url ?? firm.url,
|
||||
urlPattern: firm.urlPattern,
|
||||
openUrl: firm.url,
|
||||
signedOut: firm.signedOutPattern,
|
||||
signedOutWait: firm.signedOutWaitSeconds,
|
||||
signedOut: login?.urlContains,
|
||||
signedOutWait: login?.waitSeconds,
|
||||
};
|
||||
}
|
||||
if (step.action === 'waitFor') {
|
||||
@@ -404,8 +467,8 @@ function resolveOne(firm: Firm, step: AutomationStep): ResolvedStep {
|
||||
...step,
|
||||
urlPattern: step.urlPattern ?? firm.urlPattern,
|
||||
openUrl: firm.url,
|
||||
signedOut: firm.signedOutPattern,
|
||||
signedOutWait: firm.signedOutWaitSeconds,
|
||||
signedOut: login?.urlContains,
|
||||
signedOutWait: login?.waitSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -451,19 +514,34 @@ export function resolveSteps(firm: Firm, automation: Automation, inputs: RunInpu
|
||||
return expand(automation.steps, firm, automation, inputs);
|
||||
}
|
||||
|
||||
/** Auth steps run on the login page itself, so they get the firm's tab pattern
|
||||
* but deliberately no `signedOut` guard — that guard exists to stop ordinary
|
||||
/** Recovery steps run on the guard page itself, so they get the firm's tab
|
||||
* pattern but deliberately no `signedOut` stamp — that exists to stop ordinary
|
||||
* steps acting on a login form, and these are the exception.
|
||||
*
|
||||
* They share the expansion path, so a repeat works here too; there are no
|
||||
* per-run inputs during auth, so `timesFrom` falls back to a single pass. */
|
||||
export function resolveAuthSteps(firm: Firm): ResolvedStep[] {
|
||||
const stub: Automation = { id: '_auth', label: 'auth', description: '', steps: firm.authSteps };
|
||||
return expand(firm.authSteps, firm, stub, {}).map((step) =>
|
||||
* per-run inputs during recovery, so `timesFrom` falls back to a single pass. */
|
||||
function resolveRecoverySteps(firm: Firm, steps: AutomationStep[]): ResolvedStep[] {
|
||||
const stub: Automation = { id: '_guard', label: 'guard recovery', description: '', steps };
|
||||
return expand(steps, firm, stub, {}).map((step) =>
|
||||
step.action === 'wait' ? step : { ...step, signedOut: undefined, signedOutWait: undefined }
|
||||
);
|
||||
}
|
||||
|
||||
/** Guards as handed to the runner, recovery steps expanded. */
|
||||
export function resolveGuards(firm: Firm): ResolvedGuard[] {
|
||||
return firm.guards.map((guard) => ({
|
||||
...guard,
|
||||
recoverySteps: resolveRecoverySteps(firm, guard.recoverySteps ?? []),
|
||||
}));
|
||||
}
|
||||
|
||||
/** The pre-0.20 payload shape: the login guard's recovery steps as `authSteps`.
|
||||
* Kept so a stale runner still recovers from a signed-out redirect. */
|
||||
export function resolveAuthSteps(firm: Firm): ResolvedStep[] {
|
||||
const login = legacyLoginGuard(firm);
|
||||
return login ? resolveRecoverySteps(firm, login.recoverySteps ?? []) : [];
|
||||
}
|
||||
|
||||
/** What a step is doing, for the run log and the dashboard. */
|
||||
export function describeStep(step: AutomationStep): string {
|
||||
if (step.label) return step.label;
|
||||
|
||||
Reference in New Issue
Block a user