@blursec/sdk
v1.0.1
Published
Official isomorphic TypeScript SDK for Blursec — pre-auth credential & session leak detection backed by live stealer-log threat intel.
Maintainers
Readme
@blursec/sdk
Official isomorphic TypeScript SDK for Blursec — pre-auth credential & session leak detection backed by a live stealer-log threat-intel database.
What Blursec does
Most authentication breaches don't happen because someone "hacked" you. They happen because infostealer malware on a user's device harvested their saved credentials — for your app and every other platform, SaaS, or provider they use — into a stealer-log database, and an attacker logged into your app with a perfectly valid username + password.
Blursec sits in front of your /login endpoint and asks one question: "Is this credential already in the wild?" — answered in under 50 ms by a continuously-updated stealer-log feed. If the answer is yes, the SDK tells you what to do about it.
The SDK exposes four things:
blursec.checkCredential(email, password)— Layer 1. K-anonymity SHA-256 lookup. The raw password never leaves your server.blursec.verifySession(req)— Layer 2. Extracts a session token from a Request (Authorization header or cookie), auto-detects JWT vs opaque, hashes appropriately, and looks it up against the live stealer-log feed.blursec.webhooks.verify({ secret, payload, signature, timestamp })— Verify HMAC-SHA256-signed deliveries Blursec posts to your endpoint when a previously-clean session shows up in a fresh stealer-log batch after login.dryRun: true— Observe-only mode on any check, so you can stage Blursec into production traffic before enforcing.
Install
npm install @blursec/sdk
# or
pnpm add @blursec/sdk
# or
yarn add @blursec/sdkRequires Node 18+, a modern browser, Cloudflare Workers, Deno, or Bun. Zero runtime dependencies.
Get an API key: create a free workspace and grab your license key at blursec.dev. Full reference docs live at docs.blursec.dev.
The 4-line login endpoint
import { Blursec } from "@blursec/sdk";
const blursec = new Blursec({ apiKey: process.env.BLURSEC_API_KEY! });
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const risk = await blursec.checkCredential(email, password);
if (risk.leaked) {
return res.status(403).json({
error: "credential_compromised",
action: risk.recommendedAction, // "block" | "force_reset" | "monitor"
});
}
// ...your normal password verification continues here
});That's the integration. Everything below is detail.
How it works (prefix-blinded lookup)
The SDK never sends the raw password to Blursec. The flow:
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. SDK hashes email + ":" + password with SHA-256 locally │
│ → 64-char hex digest │
│ │
│ 2. SDK sends ONLY the first 10 hex characters (the prefix) to the API │
│ → 40 bits ≈ 1.1 trillion possible buckets; the prefix is one-way │
│ and cannot be reversed back to the password │
│ │
│ 3. API returns every stealer-log entry that hashes into that bucket │
│ → array of { hashSuffix, severity, firstSeen, sources } │
│ │
│ 4. SDK compares the remaining 54 hex chars LOCALLY │
│ → match ⇒ this exact credential is leaked │
│ │
│ 5. SDK maps severity → recommendedAction and returns to your code │
└──────────────────────────────────────────────────────────────────────────┘Same wire model as HaveIBeenPwned's range API, applied to full credential pairs rather than passwords alone, with a longer prefix so each bucket is small. The prefix is one-way (SHA-256 is preimage-resistant), but because credential pairs come from a high-entropy keyspace, a 40-bit prefix is effectively a unique identifier for the underlying credential.
Threat model. The SDK's confidentiality story relies on two trust assumptions:
- TLS terminates inside Blursec. Network observers cannot link prefixes to client IPs / accounts.
- Blursec does not log or correlate the prefixes it observes. The prefix alone leaks no credential bits, but a malicious or compromised Blursec backend that stored prefix-by-prefix request logs could in principle build a per-user lookup table out of repeated requests.
If your threat model does not allow either assumption, you should run your own copy of the stealer-log database and hash entirely on-prem.
See examples/k-anonymity.ts for a step-by-step walkthrough you can run yourself.
The CredentialRiskResult
Every credential check returns the same shape:
{
leaked: boolean; // is this credential in the database?
severity: "low" | "medium" | "high" | "critical" | null;
recommendedAction:
| "allow" | "monitor" | "force_reset" | "kill_sessions" | "block";
firstSeen: string | null; // ISO 8601, earliest sighting
sources: number; // distinct stealer-log sources
checkedAt: string; // ISO 8601, when the SDK ran the check
durationMs: number; // end-to-end SDK time
failedOpen: boolean; // true if SDK returned a safe default
}Default severity → action mapping (overridable per call):
| severity | recommendedAction |
| ---------- | ----------------- |
| critical | block |
| high | force_reset |
| medium | force_reset |
| low | monitor |
| (none) | allow |
Fail-open guarantee
If the Blursec API times out, returns 5xx, or fails for any network reason, the SDK does not throw. It returns:
{ leaked: false, recommendedAction: "allow", failedOpen: true, /* ... */ }This is intentional: a legitimate user logging in must never be locked out because of Blursec's infrastructure. You can opt out per call with failOpen: false.
const risk = await blursec.checkCredential(email, password, {
failOpen: false, // throw on timeout / 5xx
timeoutMs: 500, // tight latency budget
});Errors that always throw (and indicate a bug in your integration, not a transient failure):
BlursecAPIErrorwith status400–499(bad request, unauthorized, forbidden)BlursecValidationError(you called the SDK with invalid arguments)
See docs/errors.md.
Protective actions
The SDK does not kill sessions or flag users on your behalf — Blursec is a threat-intel API, not a session manager. Use the returned recommendedAction to drive your own session store / auth provider:
const risk = await blursec.checkCredential(email, password);
if (risk.leaked) {
switch (risk.recommendedAction) {
case "block":
// attacker likely already in — revoke every session you've issued
await yourSessionStore.revokeAllFor(userId);
return res.status(403).json({ error: "credential_compromised" });
case "force_reset":
return res.redirect("/reset-password?reason=leaked");
case "monitor":
// allow the login, but record the event for incident response
auditLog.warn("compromised_credential_login", { userId });
break;
}
}Verify live sessions (Layer 2)
Drop verifySession(req) into your authenticated-request middleware to catch a session token that surfaces in a stealer-log batch after the user already logged in. Token-type detection is automatic — JWTs are hashed by their signature segment only; opaque tokens are hashed in full.
app.use(async (req, res, next) => {
const result = await blursec.verifySession(req, {
cookieName: "sid", // optional — defaults to "session"
timeoutMs: 200, // tight — runs on every request
});
if (result.compromised && result.recommendedAction === "block") {
await yourSessionStore.revokeAllFor(req.user.id);
return res.status(401).json({ error: "session_compromised" });
}
next();
});result.tokenType ("jwt" | "opaque") is exposed so dashboards can break stats down by token shape.
Verify webhooks
Blursec posts asynchronous notifications (newly-leaked credential detected for a known customer email, session token observed in a fresh stealer-log batch, key rotated, ...) to a customer-supplied URL. Each delivery carries an HMAC-SHA256 signature plus a millisecond-precision timestamp; the SDK verifies both locally — no round-trip to Blursec.
app.post(
"/webhooks/blursec",
express.raw({ type: "*/*" }), // MUST be raw bytes — signature is over them
async (req, res) => {
const ok = await blursec.webhooks.verify({
secret: process.env.BLURSEC_WEBHOOK_SECRET!,
payload: req.body,
signature: req.header("X-Blursec-Signature") ?? "",
timestamp: req.header("X-Blursec-Timestamp") ?? "",
});
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// ...handle event
res.status(204).end();
},
);Deliveries older than 5 minutes are rejected automatically (configurable via toleranceMs).
dryRun — stage Blursec into production traffic safely
Pass dryRun: true to any credential / session check during the first 1–2 weeks of an integration. The SDK still calls the API and populates leaked / severity / enforcedAction so you can dashboard the real signal — but recommendedAction is forced to "allow" so no live traffic is blocked.
const result = await blursec.checkCredential(email, password, { dryRun: true });
metrics.increment("blursec.would_have_blocked", {
enforced: result.enforcedAction, // "block" | "force_reset" | "monitor" | "allow"
leaked: String(result.leaked),
});
// Continue with your normal login flow regardless of the verdict.Flip dryRun to false once you trust the rates.
Runtimes
The SDK uses only platform-standard APIs — fetch, AbortController, crypto.subtle.digest, TextEncoder — and ships as both ESM and CJS, with runtime-specific bundles selected by your bundler:
| Runtime | Supported | Bundle (auto-selected) | Notes |
| -------------------- | ---------- | ----------------------------- | -------------------------------------------------------------- |
| Node 20+ | ✅ | dist/index.node.{js,cjs} | Recommended. |
| Node 18+ | ✅ | dist/index.node.{js,cjs} | crypto is global; works out of the box. |
| Browsers (modern) | ⚠️ | dist/index.browser.js | Server-side only for checkCredential. Never ship API keys to the browser. |
| Cloudflare Workers | ✅ | dist/index.worker.js | See examples/cloudflare-worker.ts. |
| Vercel Edge | ✅ | dist/index.worker.js | Via the edge-light export condition. |
| Deno | ✅ | dist/index.js | npm:@blursec/sdk. |
| Bun | ✅ | dist/index.js | Native. |
Configuration
new Blursec({
apiKey: process.env.BLURSEC_API_KEY!, // required
// optional
environment: "production" | "staging" | "local",
baseUrl: "https://self-hosted.example/api",
timeoutMs: 30_000, // default request timeout (not credential check)
defaultHeaders: { "X-Tenant": "acme" },
fetch: customFetchImpl, // for retries, telemetry, mocking
// enterprise: structured audit logging (PII-scrubbed automatically)
logger: createConsoleLogger(),
logLevel: "info", // "debug" | "info" | "warn" | "error"
// enterprise: process-local circuit breaker
circuitBreaker: {
enabled: true, // default
failureThreshold: 5, // open after 5 consecutive infra failures
resetTimeoutMs: 30_000, // probe again after 30s
},
});Full reference: docs/configuration.md.
Enterprise features
The SDK ships with the surface a SOC 2 / GDPR / PDPPL-compliant deployment needs out of the box:
1. Privacy-preserving telemetry
Every value emitted to the configured logger first passes through scrubPII, which masks emails, bearer tokens, JWTs, API-key-shaped strings, long hex blobs, SSN-shaped strings, and PAN-shaped digit blocks. Sensitive object keys (authorization, cookie, password, email, hash, userId, ...) are masked regardless of value content.
import { scrubPII } from "@blursec/sdk";
scrubPII({
email: "[email protected]",
headers: { Authorization: "Bearer eyJhbGciOi..." },
note: "user 4242-4242-4242-4242",
});
// → { email: "[REDACTED]", headers: { Authorization: "[REDACTED]" }, note: "user [REDACTED]" }2. Audit-grade structured logger
Plug any structured-logging stack into the SDK by implementing the two-method BlursecLogger interface. The SDK emits named events (client.initialised, credentials.check.success, credentials.check.failed_open, circuit.opened, auth.rotated, session.killed, session.flagged, ...) so consumers can filter on stable identifiers without string-matching free-form messages.
audit-level entries are never filtered by logLevel — they always reach the sink, so the SOC 2 audit trail stays complete.
import { Blursec, createConsoleLogger } from "@blursec/sdk";
const blursec = new Blursec({
apiKey: process.env.BLURSEC_API_KEY!,
logger: createConsoleLogger(),
logLevel: "info",
});3. Circuit breaker (fail-open / fail-closed)
When the Blursec API is unhealthy every credential check would otherwise burn the full timeoutMs — multiplying /login latency at exactly the moment login traffic is most sensitive. The breaker tracks consecutive infrastructure failures (timeouts, network errors, 5xx, malformed responses) and, once the threshold trips, short-circuits subsequent calls. Those calls fail open by default with failureReason: "circuit_open", so legitimate users are never blocked.
const result = await blursec.credentials.check(email, password);
if (result.failedOpen) {
metrics.increment("blursec.failed_open", { reason: result.failureReason });
}4. Type branding
Branded primitives (ApiKey, UserId, SessionToken, RequestId, HashHex) prevent accidental mix-ups at compile time — zero runtime cost. Opt in by minting values through the matching constructors:
import { apiKey, sessionToken } from "@blursec/sdk";
const k = apiKey(process.env.BLURSEC_API_KEY!);
const s = sessionToken(req.cookies.session);
await blursec.verifySession(s); // ✅
await blursec.verifySession(k); // ❌ Argument of type 'ApiKey' is not assignable to 'RequestLike | string | SessionToken'Existing callers that pass plain string continue to work — branding is opt-in.
Reference
- docs/resources/credentials.md —
check,checkToken,checkHash, options, k-anonymity details - docs/resources/sessions.md —
verify, JWT vs opaque hashing - docs/resources/webhooks.md — HMAC-SHA256 verification, replay window
- docs/configuration.md — full constructor surface
- docs/errors.md — error hierarchy and fail-open semantics
- docs/advanced.md — custom fetch, retries (why you shouldn't on checks), telemetry, edge runtimes
Examples
- examples/login-endpoint.ts — Express drop-in (the flagship)
- examples/session-validation.ts — Layer 2 token check on every request
- examples/k-anonymity.ts — Manual hashing walkthrough
- examples/cloudflare-worker.ts — Edge integration
- examples/error-handling.ts — Strict mode (
failOpen: false) + try/catch - examples/abort-and-timeout.ts — Cancellation + per-call timeouts
- examples/custom-fetch-retry.ts — Why you should not retry credential checks
- examples/browser.ts — Browser warning (Layer 2 token checks only)
Public API
export { Blursec, BlursecClient } from "@blursec/sdk";
export {
BlursecError, BlursecAPIError, BlursecValidationError, BlursecTimeoutError,
BlursecCircuitOpenError,
AuthResource, CredentialsResource, SessionsResource, WebhooksResource,
CircuitBreaker,
// hashing
hashCredential, hashString, matchesHash, canonicalCredential,
HASH_PREFIX_LENGTH,
// session-token utilities
canonicalSessionToken, detectTokenType, extractSessionToken,
// webhook verification
verifyWebhookSignature, signWebhookPayload,
BLURSEC_WEBHOOK_HEADERS, DEFAULT_WEBHOOK_TOLERANCE_MS,
// Enterprise: structured logging
createConsoleLogger, createNoopLogger,
// Enterprise: PII scrubbing
scrubPII, scrubString, REDACTED,
// Enterprise: type branding
apiKey, userId, sessionToken, requestId, hashHex, unbrand,
} from "@blursec/sdk";
export type {
BlursecClientConfig, BlursecEnvironment,
CredentialRiskResult, CheckOptions, LeakMatch, LeakSeverity, RecommendedAction, RequestContext,
SessionVerificationResult, VerifySessionOptions,
WebhookHeaders, VerifyWebhookOptions,
AuthPrincipal, RotatedApiKey,
HashedCredential,
RequestLike, ExtractSessionTokenOptions, TokenType,
HttpMethod, RequestOptions, BlursecApiErrorPayload,
// Enterprise types
BlursecLogger, LogEntry, LogLevel, AuditEvent,
CircuitBreakerConfig, CircuitState,
ApiKey, UserId, SessionToken, RequestId, HashHex, Brand,
} from "@blursec/sdk";Documentation & links
- 📚 Full documentation: docs.blursec.dev
- 🔑 Get a license / API key: blursec.dev
- 🐛 Issues & support: github.com/blur-sec/blursec-sdk/issues
License
MIT
