@logseal/node
v0.0.2
Published
Official Node.js SDK for LogSeal - Audit logging for B2B SaaS
Maintainers
Readme
@logseal/node
Official Node.js SDK for LogSeal — Audit logging for B2B SaaS.
Installation
npm install @logseal/node
# or
pnpm add @logseal/node
# or
yarn add @logseal/nodeQuick Start
import { LogSeal } from '@logseal/node';
const logseal = new LogSeal({
apiKey: 'sk_test_...',
});
// Emit an event (batched, async)
await logseal.emit({
action: 'document.published',
organizationId: 'org_acme',
actor: {
id: 'user_123',
name: 'Jane Smith',
email: '[email protected]',
},
targets: [{
type: 'document',
id: 'doc_456',
name: 'Q3 Report',
}],
metadata: {
previousStatus: 'draft',
},
});
// Emit and wait for confirmation
const event = await logseal.emitSync({
action: 'user.deleted',
organizationId: 'org_acme',
actor: { id: 'admin_1' },
targets: [{ type: 'user', id: 'user_123' }],
});
console.log('Event ID:', event.id);
// Query events
const events = await logseal.events.list({
organizationId: 'org_acme',
action: 'document.published',
limit: 50,
});
// Auto-paginate through all events
for await (const event of logseal.events.listAll({
organizationId: 'org_acme',
})) {
console.log(event.action, event.actor.name);
}
// Graceful shutdown (flushes remaining events)
process.on('SIGTERM', async () => {
await logseal.shutdown();
process.exit(0);
});Configuration
const logseal = new LogSeal({
apiKey: 'sk_live_...', // Required
baseUrl: 'https://api.logseal.io', // Optional, default
batchSize: 100, // Events to batch before sending
flushIntervalMs: 5000, // Max time to wait before flushing
maxRetries: 3, // Retry attempts for failed requests
});Middleware
The SDK provides middleware for automatic HTTP audit logging. Every request flowing through your application is captured as an audit event — no manual emit() calls needed.
Middleware is available for Express, Fastify, and Hono. All three share the same configuration interface and are duck-typed, so they add no extra dependencies to your project.
Express
import express from 'express';
import { LogSeal } from '@logseal/node';
const app = express();
const logseal = new LogSeal({ apiKey: 'sk_live_...' });
app.use(logseal.express({
actor: (req) => ({
id: req.headers['x-user-id'] as string,
name: req.headers['x-user-name'] as string,
}),
organizationId: 'org_acme',
}));
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
// Emits action: "get.users.:id"Fastify
import Fastify from 'fastify';
import { LogSeal } from '@logseal/node';
const fastify = Fastify();
const logseal = new LogSeal({ apiKey: 'sk_live_...' });
fastify.register(logseal.fastify({
actor: (req) => ({
id: req.headers['x-user-id'] as string,
name: req.headers['x-user-name'] as string,
}),
organizationId: 'org_acme',
}));
fastify.get('/users/:id', async (req, reply) => {
return { id: req.params.id };
});
// Emits action: "get.users.:id"Hono
import { Hono } from 'hono';
import { LogSeal } from '@logseal/node';
const app = new Hono();
const logseal = new LogSeal({ apiKey: 'sk_live_...' });
app.use(logseal.hono({
actor: (c) => ({
id: c.req.header('x-user-id'),
name: c.req.header('x-user-name'),
}),
organizationId: 'org_acme',
}));
app.get('/users/:id', (c) => {
return c.json({ id: c.req.param('id') });
});
// Emits action: "get.users.:id"Middleware Configuration
All middleware accepts the same configuration options:
logseal.express({
// Required — extract the actor from each request.
// Return null to skip logging for a request.
actor: (req) => ({
id: req.user.id,
name: req.user.name,
email: req.user.email,
}),
// Required — can be a static string or a function for multi-tenant apps.
organizationId: (req) => req.headers['x-org-id'] as string,
// Paths to exclude from audit logging (supports * wildcards).
exclude: ['/health', '/metrics', '/api/internal/*'],
// Map specific routes to custom action names instead of auto-generated ones.
actionMap: {
'POST /auth/login': 'user.login',
'POST /auth/logout': 'user.logout',
'DELETE /users/:id': 'user.delete',
},
// Capture the request body in event metadata. Default: false.
captureBody: true,
// Capture the HTTP status code in event metadata. Default: true.
captureStatus: true,
// Add custom metadata to every event.
enrichMetadata: (req, metadata) => ({
...metadata,
tenantId: req.headers['x-tenant-id'],
}),
// Custom error handler. Default: console.error.
onError: (error) => myLogger.error('Audit logging failed', error),
});Action Generation
Actions are automatically generated from the HTTP method and route pattern using dot notation:
| Request | Route Pattern | Generated Action |
|---|---|---|
| GET /users | /users | get.users |
| POST /users | /users | post.users |
| GET /users/123 | /users/:id | get.users.:id |
| PUT /orgs/1/members/2 | /orgs/:orgId/members/:memberId | put.orgs.:orgId.members.:memberId |
Use actionMap to override any auto-generated action with a human-readable name. The map is checked in order: exact path first, then route pattern.
actionMap: {
'POST /users': 'user.create', // Matches POST /users exactly
'GET /users/:id': 'user.read', // Matches any GET /users/:id
}How It Works
Each middleware hooks into the framework's response lifecycle so that requests are never blocked:
- Express — listens to the
res.on('finish')event after callingnext(). - Fastify — registers an
onResponsehook via the Fastify plugin system. - Hono — calls
await next(), then emits the event after downstream handlers complete.
Events are queued into the SDK's batching system (configurable via batchSize and flushIntervalMs) and sent to the LogSeal API in bulk. If the actor or organization ID cannot be resolved for a request, the event is silently skipped.
IP addresses are resolved with fallback: req.ip (Express/Fastify) -> X-Forwarded-For header -> X-Real-IP header. The User-Agent and X-Request-ID headers are captured automatically when present.
API Reference
Events
// Emit (batched)
await logseal.emit({ action, organizationId, actor, ... });
// Emit (sync)
const event = await logseal.emitSync({ action, organizationId, actor, ... });
// List events
const events = await logseal.events.list({ organizationId, action?, limit? });
// Get single event
const event = await logseal.events.get('evt_123');
// Verify hash chain integrity
const result = await logseal.events.verify({ organizationId });Organizations
const orgs = await logseal.organizations.list();
const org = await logseal.organizations.create({ externalId: 'acme', name: 'Acme Corp' });
const org = await logseal.organizations.get('org_123');Schemas
const schemas = await logseal.schemas.list();
const schema = await logseal.schemas.create({ action: 'document.updated', ... });
const schema = await logseal.schemas.update('sch_123', { description: '...' });
await logseal.schemas.delete('sch_123');Viewer Tokens
const token = await logseal.viewerTokens.create({
organizationId: 'org_acme',
expiresIn: 3600, // 1 hour
});
// Pass token.token to your frontend for the embeddable viewerWebhooks
const webhooks = await logseal.webhooks.list();
const webhook = await logseal.webhooks.create({ url: 'https://...', events: ['*'] });
await logseal.webhooks.update('whk_123', { enabled: false });
await logseal.webhooks.delete('whk_123');Exports
const exportJob = await logseal.exports.create({
organizationId: 'org_acme',
format: 'csv',
filters: { after: '2024-01-01' },
});
// Poll until complete
const completed = await logseal.exports.poll(exportJob.id);
console.log('Download URL:', completed.download_url);Error Handling
import { LogSeal, LogSealError } from '@logseal/node';
try {
await logseal.emitSync({ ... });
} catch (error) {
if (error instanceof LogSealError) {
console.error('LogSeal error:', error.code, error.message);
console.error('Status:', error.statusCode);
}
}License
MIT
