@wade-development/security-node
v1.0.1
Published
Trusted server-side SDK for the Company security monitoring platform.
Downloads
332
Readme
@wade-development/security-node
Trusted server-side SDK for the Company security monitoring platform.
Send security-relevant events from your backend. The platform resolves which client, application and environment you are from your API key — you never send tenant identifiers, and you cannot claim to be someone else.
New to the platform? Start with docs/sdk-quick-start.md — it walks an application from "no key" to "reporting incidents" in order. This file is the reference for what the SDK does.
Install
npm install @wade-development/security-nodeQuick start
import { Security } from '@wade-development/security-node';
Security.init({
apiKey: process.env.SECURITY_API_KEY!,
endpoint: process.env.SECURITY_ENDPOINT!,
});
// later, wherever something security-relevant happens
Security.auth.loginFailed({
actor: { id: attemptedUserId },
network: { sourceIp },
metadata: { reason: 'invalid_credentials' },
});Get SECURITY_API_KEY from the security console:
Clients → your client → application → environment → Generate API key.
The key is shown once.
The one rule: never await it
Security.auth.loginSucceeded({ actor: { id: user.id } }); // correct
await Security.auth.loginSucceeded({ ... }); // pointless — returns voidtrack() and every helper return void. They buffer in memory and send in the
background. If the security platform is down, slow, or misconfigured, your
application keeps working — events are buffered, retried, and eventually
dropped rather than ever blocking a login or a payment.
The SDK does not throw. A malformed event is reported through onError and the
debug log, never as an exception in your request handler.
Configuration
Security.init({
apiKey: process.env.SECURITY_API_KEY!,
endpoint: 'https://security.company.com/ingest/v1',
batchSize: 50, // send when this many events are buffered
flushIntervalMs: 1000, // ...or after this long, whichever comes first
timeoutMs: 1500, // per-request timeout
maxQueueSize: 10_000, // hard cap; beyond this the drop policy engages
overflowStrategy: 'drop-oldest',
maxRetries: 5,
heartbeat: { enabled: true, intervalMs: 300_000, applicationVersion: '1.4.2' },
debug: false,
onError: (error, context) => myLogger.warn('security sdk', { error, context }),
});Every value is clamped to a sane range, so a typo cannot produce a 10-second timeout on your login path.
Environment variables
| Variable | Purpose |
| --- | --- |
| SECURITY_API_KEY | Used when apiKey is not passed to init() |
| SECURITY_ENDPOINT | Used when endpoint is not passed |
| SECURITY_DEBUG | true enables verbose SDK logging |
Request context
Set the actor and network details once per request; everything tracked inside inherits them.
Security.withContext(
{
actor: { id: user.id, role: user.role },
session: { id: sessionId },
network: { sourceIp, userAgent },
request: { requestId },
},
async () => {
await handleRequest(); // any Security.* call in here gets the context
},
);Explicit values always win, so an admin acting on another account still records the right actor.
Nuxt / Nitro
// server/plugins/security.ts
import { Security } from '@wade-development/security-node';
export default defineNitroPlugin(() => {
Security.init({
apiKey: process.env.SECURITY_API_KEY!,
endpoint: process.env.SECURITY_ENDPOINT!,
});
});// server/middleware/security-context.ts
import { securityRequestMiddleware } from '@wade-development/security-node/nitro';
export default defineEventHandler(
securityRequestMiddleware({
// Only enable when a proxy really is in front of you — otherwise callers
// can forge their own source IP.
trustProxy: true,
resolveActor: (event) => {
const user = event.context.user;
return user ? { id: user.id, role: user.role } : undefined;
},
}),
);Express
app.use((req, res, next) => {
Security.withContext(
{
actor: req.user ? { id: req.user.id, role: req.user.role } : undefined,
network: { sourceIp: req.ip, userAgent: req.get('user-agent') },
request: { requestId: req.id },
},
() => next(),
);
});Fastify
fastify.addHook('onRequest', (request, _reply, done) => {
Security.withContext(
{
network: { sourceIp: request.ip, userAgent: request.headers['user-agent'] },
request: { requestId: request.id },
},
() => done(),
);
});Event catalogue
Helpers exist for the events the platform's detection rules match on. Prefer
them over raw track() — they cannot get the event type wrong.
| Helper | Event type | Feeds |
| --- | --- | --- |
| Security.auth.loginFailed() | authentication.login.failed | AUTH-001, AUTH-002, AUTH-003 |
| Security.auth.loginSucceeded() | authentication.login.succeeded | AUTH-004, AUTH-005, AUTH-006 |
| Security.auth.logout() | authentication.logout | |
| Security.auth.mfaFailed() | authentication.mfa.failed | MFA-001 |
| Security.auth.mfaDisabled() | authentication.mfa.disabled | MFA-002, MFA-003 |
| Security.identity.passwordChanged() | identity.password.changed | |
| Security.identity.roleChanged() | authorization.role.changed | IAM-001, IAM-002, IAM-004 |
| Security.authorization.denied() | authorization.access.denied | API-001 |
| Security.admin.action() | administration.setting.changed | IAM-002 |
| Security.admin.securityFeatureDisabled() | administration.security_feature.disabled | ADMIN-001 |
| Security.admin.auditLoggingDisabled() | administration.audit_logging.disabled | ADMIN-002 |
| Security.data.bulkExport() | data_access.bulk_export | IAM-003, DATA-001, DATA-002 |
| Security.data.bulkDelete() | data_change.bulk_delete | DATA-003 |
| Security.session.newDevice() | session.new_device | DATA-002 |
| Security.session.reusedAfterRevocation() | session.reused_after_revocation | SESSION-002 |
| Security.api.rateLimitExceeded() | api_security.rate_limit.exceeded | API-001 |
| Security.api.validationFailed() | api_security.input_validation.failed | API-003 |
| Security.secret.apiKeyCreated() | secret.api_key.created | SECRET-001 |
For anything not listed, use Security.track({ type: 'category.object.action' }).
Types must be lowercase, dot-separated, and start with one of the 14 platform
categories.
What not to send
The SDK strips these before anything leaves your process, but do not put them in
metadata in the first place:
passwords, access and refresh tokens, session cookies, API keys, authorization headers, payment card data, and personal data you do not need for an investigation.
Send identifiers, not payloads: { recordId: 4821 }, not the record.
Graceful shutdown
process.on('SIGTERM', async () => {
await Security.close(); // stops timers, makes a final flush attempt
await server.close();
});The SDK also flushes on beforeExit, and all its timers are unref'd — it will
never hold your process open.
Health
const health = Security.health();
// { initialized, queueSize, eventsSent, eventsDropped, eventsFailed, retries, lastError }Worth exposing on your own /health endpoint: a rising eventsDropped means
the platform is unreachable and you are losing security signal.
Retry behaviour
| Response | Behaviour |
| --- | --- |
| 202 | Delivered |
| 408, 429, 5xx, network error | Retried with exponential backoff and jitter (250ms → 5s), bounded by maxRetries. Retry-After is honoured |
| 400, 401, 403, 422 | Not retried — the batch is discarded and reported through onError |
A wrong API key produces one clear error, not an infinite retry loop.
