steadfast-fraud
v1.1.0
Published
Check customer fraud/delivery stats on Steadfast Courier by phone number — handles login, session cookies, host failover, multi-account failover, and 24h auto-refresh automatically.
Maintainers
Readme
fraudjs
Check a customer's delivery and fraud history on Steadfast Courier by phone number. Drop in credentials and just call checkPhone() — login, cookies, host failover, and session refresh are handled automatically.
import { checkPhone } from 'steadfast-fraud';
const result = await checkPhone('01700000000');
// { delivered: 12, cancelled: 1, frauds: 0, total: 13, successRatio: 0.9231, consignment: [...] }Requirements
- Node.js ≥ 18 (uses built-in fetch and crypto)
- An active Steadfast merchant account
Install
npm install steadfast-fraudSetup
You need at least one Steadfast merchant credential. Two ways to add them:
Option A — .env (quickest)
[email protected]:yourpasswordMultiple accounts for failover (comma-separated, no spaces):
[email protected]:pass1,[email protected]:pass2Make sure your app loads .env (e.g. with dotenv) or export the variable in your shell.
Option B — addCredential() / CLI (persisted)
Credentials are encrypted with AES-256-GCM and stored in your OS config directory:
- Linux:
~/.config/fraudjs/credentials.json - macOS:
~/Library/Preferences/fraudjs/credentials.json - Windows:
%APPDATA%\fraudjs\credentials.json
npx fraudjs add-credential
# prompts for email + password (hidden input)Or from code:
import { addCredential } from 'steadfast-fraud';
await addCredential({ email: '[email protected]', password: 'yourpassword' });Encryption key
By default a random 32-byte key is generated and saved to ~/.config/fraudjs/master.key. It's machine-specific — credentials can't be decrypted on another machine unless you copy the key file.
For a portable/server setup, set FRAUDJS_SECRET to any string and the key will be derived from it instead:
# generate a good one:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
FRAUDJS_SECRET=<that value>Configuration
All optional — sensible defaults are built in.
| Variable | Default | What it does |
|---|---|---|
| FRAUDJS_CREDENTIALS | – | email:password pairs, comma-separated |
| FRAUDJS_SECRET | machine key | Key for the encrypted credential store |
| FRAUDJS_BASE_URL | steadfast.com.bd, packzy.com, merchant.packzy.com | Host(s) to try, in order. Comma-separated; scheme optional |
| FRAUDJS_TIMEOUT_MS | 30000 | Per-request timeout |
| FRAUDJS_RETRIES | 2 | Retries for network errors and transient 5xx (never applied to POST /login) |
merchant.packzy.com — the original host — now returns 502 from its load
balancer. It is kept last in the default list for backwards compatibility, so
existing setups keep working while new lookups go to the live host.
API
checkPhone(phone)
Returns delivery and fraud stats for a phone number. Tries each configured credential in order; if one fails it moves to the next.
The number is normalized first, so all of these are equivalent:
await checkPhone('01712345678');
await checkPhone('+8801712345678');
await checkPhone('017-1234 5678');Returns:
{
delivered: number, // total successfully delivered orders
cancelled: number, // total cancelled orders
frauds: number, // flagged fraud orders
total: number, // delivered + cancelled
successRatio: number, // delivered / total, 0 when there is no history
consignment: object[], // raw consignment objects from the Steadfast API
}Throws:
FraudJSError— the phone number isn't a valid Bangladeshi mobile numberNoCredentialsError— nothing configuredAllCredentialsFailedError— every credential was tried and failed (has.failuresarray)ServiceUnavailableError— no configured host served the login page (portal down or moved)RateLimitError— login throttled (has.retryAfterSeconds)NetworkError— DNS failure, timeout, etc.
addCredential({ email, password })
Encrypts and saves a credential. Replaces any existing entry for that email.
listCredentials()
Returns stored emails only — passwords are never exposed.
removeCredential(email)
Removes a stored credential. Throws if the email isn't found.
refresh()
Forces a cookie refresh for all configured accounts. You normally don't need this — sessions auto-refresh every 24 hours.
diagnose()
Checks every configured host without logging in, and reports which credentials are visible. Use it to tell "the portal moved" apart from "the password is wrong".
import { diagnose } from 'steadfast-fraud';
const report = await diagnose();
// {
// hosts: [
// { baseUrl: 'https://steadfast.com.bd', status: 200, ok: true, csrfToken: true, ms: 165, error: null },
// { baseUrl: 'https://merchant.packzy.com', status: 502, ok: false, csrfToken: false, ms: 152, error: 'HTTP 502' },
// ],
// credentials: ['[email protected]'],
// ok: true,
// }Error handling
All errors extend FraudJSError and have a .name matching the class name (useful for switch statements without instanceof):
import {
checkPhone,
NoCredentialsError,
AllCredentialsFailedError,
AuthError,
NetworkError,
ServiceUnavailableError,
RateLimitError,
} from 'steadfast-fraud';
try {
return await checkPhone(phone);
} catch (err) {
switch (err.name) {
case 'NoCredentialsError':
console.error('No credentials configured');
break;
case 'AllCredentialsFailedError':
for (const { email, error } of err.failures) {
console.error(`[${email}] ${error}`);
}
break;
case 'ServiceUnavailableError':
console.error('Steadfast portal is down or moved — run `fraudjs doctor`');
break;
case 'RateLimitError':
console.error(`Throttled; retry in ${err.retryAfterSeconds ?? 60}s`);
break;
case 'NetworkError':
console.error('Cannot reach Steadfast');
break;
default:
throw err;
}
}CLI
npx fraudjs check 01700000000
npx fraudjs doctor
npx fraudjs add-credential
npx fraudjs list-credentials
npx fraudjs remove-credential [email protected]
npx fraudjs refreshcheck prints JSON to stdout. Exit code 1 + stderr message on failure.
doctor reports host reachability and configured credentials — run this first when
lookups start failing:
$ npx fraudjs doctor
Hosts:
ok https://steadfast.com.bd HTTP 200 165ms
ok https://packzy.com HTTP 200 174ms
FAIL https://merchant.packzy.com HTTP 502 152ms HTTP 502
Credentials:
[email protected]How it works
Steadfast uses a standard Laravel session setup. The sequence on first use:
- Pick a host — each entry in
FRAUDJS_BASE_URL(or the built-in list) is tried in order until one serves the login page. The host that accepts the login is remembered for the rest of the session, since the session cookie is only valid there. GET /login— pick up theXSRF-TOKENcookie and the_tokenCSRF value from the HTMLPOST /loginwithredirect: 'manual'— capture thesteadfast_courier_sessioncookie from the 302 before following it (if fetch auto-follows, those Set-Cookie headers are lost)- Follow the redirect to complete the session
GET /user/consignment/getbyphone/{phone}— send both cookies plusX-XSRF-TOKENset to the URL-decoded value of the XSRF cookie (Laravel writes it encoded but expects it decoded in the header)
Sessions are cached in memory. If a request returns HTML or a 401/419, the session is refreshed and the request is retried once before trying the next credential.
Only unreachable hosts trigger failover. A rejected password fails immediately, so bad credentials never burn the login throttle across every mirror.
Using with Express / Next.js
The singleton client keeps sessions alive between requests, so only the first call per credential triggers a login:
// Express
app.get('/check/:phone', async (req, res) => {
try {
res.json(await checkPhone(req.params.phone));
} catch (err) {
res.status(502).json({ error: err.message, failures: err.failures });
}
});
// Next.js App Router (Node runtime)
export async function GET(req) {
const phone = new URL(req.url).searchParams.get('phone');
if (!phone) return Response.json({ error: 'phone required' }, { status: 400 });
try {
return Response.json(await checkPhone(phone));
} catch (err) {
return Response.json({ error: err.message }, { status: 502 });
}
}Troubleshooting
AllCredentialsFailedError: All N credential(s) failed
Check err.failures for per-account details. Common causes: wrong password, account locked, or rate limiting. Add more accounts with fraudjs add-credential.
ServiceUnavailableError: No Steadfast host was reachable
No configured host served the login page. Run npx fraudjs doctor to see the status of each. If Steadfast has moved to a new origin, point FRAUDJS_BASE_URL at it:
FRAUDJS_BASE_URL=https://new-host.example npx fraudjs check 01700000000AuthError: Could not extract CSRF token from …
The host answered 200 but the login form markup changed. Open the URL in the error message and check for a <meta name="csrf-token"> tag or a hidden _token input.
AuthError: Login failed … — check email/password
Credentials were rejected. Verify by logging in to https://steadfast.com.bd manually.
RateLimitError: Too many login attempts
Steadfast throttles /login to roughly 10 attempts per window. Wait it out (err.retryAfterSeconds) rather than retrying in a loop.
Failed to decrypt credentials — wrong FRAUDJS_SECRET or corrupted store
The FRAUDJS_SECRET doesn't match the one used when the credentials were originally stored. Restore the original secret, or remove and re-add credentials.
Response always shows zeros
Phone number likely has no orders in Steadfast. Check result.consignment for the raw API payload.
Tests
npm testUses Node's built-in node:test — no extra dependencies. All tests mock globalThis.fetch, no network needed.
License
MIT
Note: This package automates access to the Steadfast merchant portal. You're responsible for complying with their terms. Use only accounts you own and authorise.
