@fingerprintchecker/node
v0.5.0
Published
FingerPrintChecker server SDK for Node.js: API client, sealed result decryption, webhook signature verification.
Readme
@fingerprintchecker/node
Server SDK for FingerPrintChecker. Zero runtime dependencies (built-in fetch and node:crypto), Node 18+, ESM and CommonJS.
import { FpChecker, unsealResult, verifyWebhookSignature, FpCheckerApiError } from '@fingerprintchecker/node';
const fp = new FpChecker({ secretKey: process.env.FPCHECKER_SECRET_KEY! });
const verdict = await fp.checkEligibility({ visitorId: 'v_…' });
if (verdict.ok === false) console.log('refused:', verdict.error ?? verdict.reason);Client
One async method per OpenAPI operationId: createIntent, getChallenge, identify, botCheck, checkEligibility, releaseEligibility, checkQuota, bindSubject, relateSubjects, sendFeedback, getClaimFlags, searchEvents, getEvent, updateEvent, getVisitor, deleteVisitor, getHealth, getStatus.
- The secret key goes in the
X-Secret-Keyheader, never in a URL.getChallengeandidentifyuse the publishable key (publishableKeyoption). - 2xx responses are returned as data, including refusals such as
{ ok: false, error }. Checkok. - Non-2xx throws
FpCheckerApiErrorwithstatus,code(error ?? reasonfrom the body) andretryAfter(seconds). - Network failures and timeouts also throw
FpCheckerApiError, withstatus0,codenetwork_errorortimeout, and the original error ascause. - Redirects are never followed (they would forward the secret key to another origin).
baseUrlmust be https, except localhost, 127.0.0.1 and [::1]. - Only GET requests retry (including after network failures and timeouts) (429 and 5xx, at most 2 retries, honouring
Retry-After, wait capped at 10 s). POST, PATCH and DELETE are never retried.
Bot check
botCheck classifies a request your backend received (header-only: no script ran in the sender). It is not charged and writes no event. Forward the original URL (the host and scheme the visitor used, not your origin behind a CDN or load balancer) and the client IP. botCheckHeaders keeps only the headers the check needs and drops Cookie, Authorization and everything else.
import express from 'express';
import { FpChecker, botCheckHeaders } from '@fingerprintchecker/node';
const fp = new FpChecker({ secretKey: process.env.FPCHECKER_SECRET_KEY! });
const app = express();
app.set('trust proxy', 1); // number of proxy hops in front of you (or a CIDR list), never `true`: req.ip must not be spoofable
app.use(async (req, res, next) => {
try {
const verdict = await fp.botCheck({
method: req.method,
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`, // original host and scheme
ip: req.ip!,
headers: botCheckHeaders(req.headers),
});
if (verdict.bot.result === 'bad') return res.status(403).end();
} catch {
// Failure policy: fail open. An outage or rate limit of the check must not take your site down.
}
next(); // 'good' = a verified or signed directory bot; verdict.bot.info says which
});bot.result is good, bad or not_detected; bot.info (id, name, provider, category, identity) is set when the request named a directory bot. A 400 throws FpCheckerApiError with code invalid_request or invalid_ip and the offending field in body.
Rules of use:
- Call
botCheckonce per request and reuse the answer; do not call it again for the same request. - Never retry a signed request: a retry replays the signature nonce and the request is then judged
spoofed(bad). ipmust be the trusted client IP, not an address a client can set. With Express settrust proxyto your proxy hop count or CIDR list (nevertrue), then usereq.ip.- Limits: 3,000 checks per minute per client IP and 1,200 per minute per project (429 beyond that).
botCheckHeadersalso forwards any header named in the request'ssignature-input, so signed agents verify; Cookie and Authorization are never forwarded.
Event signals come in two versions: signals.version 1 (before server 0.48.0, no good, no bot.info) and 2. searchEvents also filters by botResult, botCategory, botIdentity and botId.
Sealed results
const result = unsealResult(identifyResponse.sealedResult, [process.env.FPC_SEALED_KEY!]);Pass every active key (active and previous during rotation). Throws SealedResultError with code of bad_format, unknown_key, decrypt_failed, invalid_key or unsupported_version.
Webhooks
// rawBody must be the exact bytes received, not a re-serialised object.
verifyWebhookSignature({ rawBody, header: req.headers['fpc-signature'] as string, secret: process.env.FPC_WEBHOOK_SECRET! });Throws WebhookSignatureError with code of malformed_header, bad_signature, expired or future (default tolerance 300 s; toleranceSec must be a finite number >= 0 or a TypeError is thrown).
License
Apache-2.0
