Can a client log in and reach the correct page?
A healthy web server and a successful login endpoint do not prove that a client can complete the login journey and reach the expected business page. This verifier uses a real browser to authenticate, follows the application flow, and confirms that the expected destination and business content are actually available.
Configuration
Environment: LOGIN_URL, LOGIN_USER, LOGIN_PASSWORD, EXPECTED_URL, EXPECTED_TEXT
Optional selector overrides: USER_SELECTOR, PASSWORD_SELECTOR, SUBMIT_SELECTOR. Requires Playwright, its Chromium browser, and Node.js 24 or later.
Recipe structure
The top-level executable entry file sits beside a same-named directory that owns the canonical implementation and its dependency manifest. Run npm install once in that directory and retain the generated package-lock.json:
verifiers/
├── verify-client-login.js
└── verify-client-login/
├── package.json
└── verify-client-login.jsThe canonical contract stays at the boundary.
The implementation exports the canonical verifier object: id, label, schedule.interval, schedule.timeout, and asynchronous verify() returning structured summary and steps. The top-level file exports that exact object unchanged, adds validated standalone execution, and is what BRS-Core discovers.
Top-level executable entry file
verifiers/verify-client-login.js
#!/usr/bin/env node
'use strict';
const verifier = require('./verify-client-login/verify-client-login');
module.exports = verifier;
if (require.main === module) {
void runStandalone(verifier);
}
async function runStandalone(standaloneVerifier) {
let timeoutHandle;
try {
const timeout = standaloneVerifier.schedule?.timeout ?? 30;
if (!Number.isInteger(timeout) || timeout <= 0) {
throw new Error('schedule.timeout must be a positive integer');
}
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
const error = new Error(
`Standalone verifier execution timed out after ${timeout} seconds.`
);
error.code = 'BRS_TIMEOUT';
reject(error);
}, timeout * 1000);
});
const result = await Promise.race([
Promise.resolve().then(() => standaloneVerifier.verify()),
timeoutPromise
]);
validateResult(result);
await writeStream(
process.stdout,
`${JSON.stringify(result, null, 2)}\n`
);
process.exit(result.steps.some((step) => !step.success) ? 1 : 0);
} catch (error) {
const summary = error?.code === 'BRS_TIMEOUT'
? error.message
: error?.code === 'BRS_INVALID_RESULT'
? 'Standalone verifier returned a malformed result.'
: 'Standalone verifier execution failed unexpectedly.';
await Promise.all([
writeStream(
process.stdout,
`${JSON.stringify({ summary, steps: [] }, null, 2)}\n`
),
writeStream(process.stderr, `${sanitize(error?.message)}\n`)
]);
process.exit(1);
} finally {
clearTimeout(timeoutHandle);
}
}
function validateResult(result) {
const names = new Set();
const valid = result &&
typeof result === 'object' &&
!Array.isArray(result) &&
typeof result.summary === 'string' &&
result.summary.trim() !== '' &&
Array.isArray(result.steps) &&
result.steps.length > 0 &&
result.steps.every((step) => {
const uniqueName = step &&
/^[a-z][a-z0-9_]*$/.test(step.name) &&
!names.has(step.name);
if (uniqueName) names.add(step.name);
return uniqueName &&
typeof step.label === 'string' &&
step.label.trim() !== '' &&
typeof step.success === 'boolean' &&
Number.isFinite(step.durationSeconds) &&
step.durationSeconds >= 0 &&
typeof step.message === 'string' &&
step.message.trim() !== '' &&
typeof step.checkedAt === 'string' &&
Number.isFinite(Date.parse(step.checkedAt));
}) &&
(result.observations === undefined ||
Array.isArray(result.observations));
if (!valid) {
const error = new Error('verifier result is malformed');
error.code = 'BRS_INVALID_RESULT';
throw error;
}
}
function writeStream(stream, value) {
return new Promise((resolve, reject) => {
stream.write(value, (error) => error ? reject(error) : resolve());
});
}
function sanitize(value) {
return String(value || 'Unknown error')
.split(/\r?\n/, 1)[0]
.replace(/[\u0000-\u001f\u007f]/g, ' ')
.slice(0, 500);
}Verifier implementation
verifiers/verify-client-login/verify-client-login.js
'use strict';
const { chromium } = require('playwright');
const path = require('node:path');
module.exports = {
id: 'client_login_capability',
label: 'Client Login Capability',
schedule: {
interval: 300,
timeout: 60
},
async verify() {
loadLocalEnvironment();
const loginUrl = required('LOGIN_URL');
const loginUser = required('LOGIN_USER');
const loginPassword = required('LOGIN_PASSWORD');
const expectedUrl = required('EXPECTED_URL');
const expectedText = required('EXPECTED_TEXT');
const startedAt = Date.now();
const steps = [];
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(loginUrl, {
waitUntil: 'domcontentloaded'
});
await page.locator(
process.env.USER_SELECTOR || '#username'
).fill(loginUser);
await page.locator(
process.env.PASSWORD_SELECTOR || '#password'
).fill(loginPassword);
await page.locator(
process.env.SUBMIT_SELECTOR ||
'button[type="submit"]'
).click();
await page.waitForURL(expectedUrl, {
timeout: 15000
});
await page.getByText(expectedText, {
exact: false
}).first().waitFor({
state: 'visible',
timeout: 10000
});
steps.push(step(
'verify_client_login',
'Verify Client Login',
true,
startedAt,
'The client logged in and reached the expected page.'
));
} catch (error) {
logFailure('verify_client_login', error);
steps.push(step(
'verify_client_login',
'Verify Client Login',
false,
startedAt,
'The client could not reach the expected page after login.'
));
} finally {
if (browser) await browser.close();
}
return {
summary: steps[0].success
? 'The client login capability is working as expected.'
: 'The client login capability is not working as expected.',
steps
};
}
};
function loadLocalEnvironment() {
try {
process.loadEnvFile(path.join(__dirname, '.env'));
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error;
}
}
function required(name) {
const value = String(process.env[name] || '').trim();
if (!value) throw new Error(`${name} is required`);
return value;
}
function logFailure(name, error) {
const detail = sanitize(error instanceof Error ? error.message : error);
console.error(
`verifier=client_login_capability step=${name} error="${detail}"`
);
}
function sanitize(value) {
return String(value || 'Unknown error')
.split(/\r?\n/, 1)[0]
.replace(/[\u0000-\u001f\u007f]/g, ' ')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.slice(0, 500);
}
function step(name, label, success, startedAt, message) {
return {
name,
label,
success,
durationSeconds: Number(
((Date.now() - startedAt) / 1000).toFixed(3)
),
message,
checkedAt: new Date().toISOString()
};
}Dependency manifest
verifiers/verify-client-login/package.json
{
"name": "brs-verifier-client-login",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"verify": "node ../verify-client-login.js"
},
"dependencies": {
"playwright": "1.62.1"
},
"engines": {
"node": ">=24"
}
}Run it directly
cd verify-client-login npm install npx playwright install chromium npm run verify
A passing run writes validated structured JSON to standard output and exits 0. A business failure includes a failed step and exits 1. A timeout, malformed result, or unexpected execution error writes a runner-generated JSON result and exits 1.
Run it continuously with BRS-Core
This verifier does not require BRS-Core to execute. Direct execution is part of the verifier contract and makes development, troubleshooting, CI/CD use, and manual diagnostics straightforward.
BRS-Core can run this verifier alongside your other approved verifiers continuously as part of 24/7 business capability monitoring. Because every recipe presents the same canonical contract, Core can discover and schedule these verifiers together, run them independently, enforce their timeouts, record their results, and expose their signals to the monitoring environment.