Session guard
A valid cookie does not prove the owner is still using it.
Bind after login
Call from your backend after successful authentication. Use an opaque session identifier instead of the cookie itself. Keep your secret server-side. Only bind after authentication: binding again resets the trusted device and the binding time.
POST https://nolgard.com/v1/session
{"secret":"sk_…","event":"bind","sessionId":"sess_42","accountId":"user_42","token":"BROWSER_TOKEN","ip":"198.51.100.8","userAgent":"Mozilla/5.0 … Chrome/130.0.0.0 Safari/537.36","country":"US"}
{"decision":"allow","reasons":[],"boundAt":1792800000000}Get the optional browser token with await Nolgard.execute({action:'login'}). Tokens must belong to the site and be unexpired. Session and account ids are nonempty strings up to 256 characters. Optional fields: token (32768 characters), ip (128), userAgent (2048), country (two-letter ISO code).
Check before sensitive actions
Check before billing changes, API-key creation, email or password changes, and exports. Read the account and session identifiers from your authenticated server session instead of untrusted form fields. Send a fresh browser token and the end user's User-Agent; your server's own HTTP-client User-Agent is the wrong value here.
POST /v1/session
{"secret":"sk_…","event":"check","sessionId":"sess_42","accountId":"user_42","token":"FRESH_BROWSER_TOKEN","ip":"203.0.113.4","userAgent":"curl/8.0","country":"US"}
{"decision":"revoke","reasons":["session_tool_ua","session_ua_change"],"boundAt":1792800000000}Your backend must enforce the result: allow the action, require independent step-up for challenge, or invalidate the session for revoke. Nolgard cannot delete your application's cookie.
Reasons
| Reason | Condition | Decision |
|---|---|---|
| session_device_change | Both known visitor and fingerprint keys differ | revoke |
| session_tool_ua | Browser binding is now an HTTP library or headless tool | revoke |
| session_ua_change | Browser family or OS differs | challenge |
| session_country_jump | Country differs from binding, previous check (or bind) less than two hours ago | challenge |
| session_network_change | Both IP prefix and known ASN differ | allow alone |
| session_unknown | No current binding for session and account | allow |
The strongest decision wins. Unknown sessions return boundAt:null. Version upgrades in the same browser family and OS do not trigger a UA change. Missing observations do not count as changed observations.
Node SDK
const nf = createNolgard({ secret: process.env.NOLGARD_SECRET });
await nf.session.bind({ sessionId, accountId, token, ip, userAgent, country });
const result = await nf.session.check({ sessionId, accountId, token, ip, userAgent, country });
if (result.decision === 'revoke') return invalidateSession();
if (result.decision === 'challenge') return requireIndependentStepUp();Both methods fail open on timeout, network failure, 429 or 5xx: {decision:'allow',reasons:[],boundAt:null,failOpen:true}. Invalid secrets and other 4xx errors throw.
Webhooks and retention
Challenge and revoke checks queue session.suspicious with {accountId,sessionPrefix,reasons,decision}. Only the first 12 characters of the session HMAC are included, never the raw session id. The site dashboard shows the latest ten suspicious checks with account hash, reasons, decision and time. Session bindings expire 30 days after last_seen; suspicious checks are retained 30 days. Scheduled pruning removes expired rows.
Invalid input or token returns 400; invalid secret 401; demo writes 403; more than 120 requests per minute per site 429. All four public test secrets return write-free allow results; check returns session_unknown, bind returns no reasons and null boundAt.
Limits
Rules and the two-hour window are logical defaults rather than measured thresholds. VPN toggles, corporate proxies and mobile roaming can change location or network. Network changes alone never challenge. Browser updates can change fingerprints; browser version alone is not a UA change. Device-key comparison needs browser tokens on both bind and check. ASN comes from authenticated browser-token metadata, never your backend's connection. Without a token, ASN is unknown. A missing token cannot prove device continuity. Attackers who steal browser identifiers too may evade this check. Session and account ids and IP prefixes are hashed at rest; raw IPs and User-Agent strings are not stored.