Package Exports
- untamper-sdk
Readme
unTamper Node.js SDK
Official Node.js SDK for unTamper - Enterprise audit logging platform for secure, write-once-read-only audit logs.
Features
- ๐ Fast & Reliable: Optimized for high-performance log ingestion
- ๐ Type-Safe: Full TypeScript support with comprehensive type definitions
- ๐ Cryptographic Verification: Blockchain-style log verification with hash chaining
- ๐ Query & Filter: Powerful querying with multiple filters and pagination
- ๐ Auto-Retry: Built-in retry logic with exponential backoff
- ๐ก๏ธ Error Handling: Comprehensive error handling with custom error classes
- ๐ฆ Zero Dependencies: Minimal footprint with no external runtime dependencies
- ๐งช Well Tested: Comprehensive test suite with 100% coverage
- ๐ง Developer Friendly: Easy configuration and debugging support
Installation
npm install @untamper/sdk-nodeQuick Start
import { UnTamperClient } from '@untamper/sdk-node';
// Initialize the client
const client = new UnTamperClient({
projectId: 'your-project-id',
apiKey: 'your-api-key',
// For development, you can override the base URL
baseUrl: 'http://localhost:3000',
});
// Log an audit event
const response = await client.logs.ingestLog({
action: 'user.login',
actor: {
id: 'user123',
type: 'user',
display_name: 'John Doe',
},
result: 'SUCCESS',
context: {
request_id: 'req_123456',
session_id: 'sess_789012',
},
metadata: {
version: '1.0.0',
environment: 'production',
},
});
console.log('Log ingested:', response.ingestId);Configuration
Required Settings
projectId: Your project identifierapiKey: Your project API key
Optional Settings
baseUrl: API base URL (defaults to production, allows dev override)timeout: Request timeout in milliseconds (default: 30000)retryAttempts: Number of retry attempts (default: 3)retryDelay: Delay between retries in milliseconds (default: 1000)
const client = new UnTamperClient({
projectId: 'your-project-id',
apiKey: 'your-api-key',
baseUrl: 'http://localhost:3000', // For development
timeout: 10000,
retryAttempts: 5,
retryDelay: 2000,
});API Reference
Log Ingestion
client.logs.ingestLog(request)
Ingests a single audit log.
Example:
const response = await client.logs.ingestLog({
action: 'document.update',
actor: {
id: 'user123',
type: 'user',
display_name: 'John Doe',
},
target: {
id: 'doc456',
type: 'document',
display_name: 'Important Document',
},
result: 'SUCCESS',
changes: [
{
path: 'title',
old_value: 'Old Title',
new_value: 'New Title',
},
],
context: {
request_id: 'req_123',
client: 'web-app',
},
metadata: {
feature: 'document-editor',
version: '2.1.0',
},
});client.logs.ingestLogs(requests)
Ingests multiple audit logs in batch.
const responses = await client.logs.ingestLogs([
{ action: 'user.login', actor: { id: 'user1', type: 'user' } },
{ action: 'user.logout', actor: { id: 'user2', type: 'user' } },
]);client.logs.checkIngestionStatus(ingestId)
Checks the status of a previously submitted audit log.
const status = await client.logs.checkIngestionStatus('ingest_123');
console.log('Status:', status.status); // PENDING, PROCESSING, COMPLETED, FAILED, RETRYINGclient.logs.waitForCompletion(ingestId, options)
Waits for an ingestion to complete with polling.
const status = await client.logs.waitForCompletion('ingest_123', {
pollInterval: 1000, // Check every 1 second
maxWaitTime: 30000, // Wait up to 30 seconds
});client.logs.queryLogs(options)
Queries audit logs with optional filters and pagination.
Parameters:
limit(optional): Number of logs to return (default: 50)offset(optional): Pagination offset (default: 0)action(optional): Filter by action (case-insensitive)result(optional): Filter by result (SUCCESS, FAILURE, DENIED, ERROR)actorId(optional): Filter by actor IDactorType(optional): Filter by actor typetargetId(optional): Filter by target IDtargetType(optional): Filter by target type
Example:
// Query all logs
const allLogs = await client.logs.queryLogs();
// Query with filters
const failedLogins = await client.logs.queryLogs({
action: 'user.login',
result: 'FAILURE',
limit: 10,
});
// Query by actor
const userActions = await client.logs.queryLogs({
actorId: 'user_123',
actorType: 'user',
});
// Pagination
const page2 = await client.logs.queryLogs({
limit: 50,
offset: 50,
});
console.log('Logs:', allLogs.logs);
console.log('Total:', allLogs.pagination.total);
console.log('Has more:', allLogs.pagination.hasMore);Log Verification
Important: All verification is done client-side for maximum security. This means you can verify logs without trusting the server, ensuring true cryptographic integrity.
client.initialize()
Initializes the client by fetching the ECDSA public key for verification. This must be called before any verification operations.
// Initialize client (fetch public key)
await client.initialize();client.verification.verifyLog(log)
Verifies the cryptographic integrity of a single log using client-side verification.
Parameters:
log(required): The AuditLog object to verify
Example:
// First, initialize the client
await client.initialize();
// Query logs
const logs = await client.logs.queryLogs({ limit: 1 });
const log = logs.logs[0];
// Verify single log
const verification = await client.verification.verifyLog(log);
console.log('Valid:', verification.valid);
console.log('Hash valid:', verification.hashValid);
console.log('Signature valid:', verification.signatureValid);
if (verification.error) {
console.log('Error:', verification.error);
}client.verification.verifyLogs(logs)
Verifies multiple logs with blockchain-style chain validation.
Parameters:
logs(required): Array of AuditLog objects to verify
Example:
// First, initialize the client
await client.initialize();
// Query multiple logs
const logs = await client.logs.queryLogs({ limit: 10 });
// Verify with chain validation (blockchain-style)
const chainVerification = await client.verification.verifyLogs(logs.logs);
console.log('Chain valid:', chainVerification.valid);
console.log('Total logs:', chainVerification.totalLogs);
console.log('Valid logs:', chainVerification.validLogs);
console.log('Invalid logs:', chainVerification.invalidLogs);
if (chainVerification.brokenAt) {
console.log('Chain broken at sequence:', chainVerification.brokenAt);
}
if (chainVerification.errors.length > 0) {
console.log('Errors:', chainVerification.errors);
}Why Client-Side Verification?
- โ Trustless: Verify without trusting the server
- โ Offline: Verify logs without API calls (after fetching public key)
- โ Independent: Third parties can verify logs
- โ Tamper-proof: Mathematical proof of integrity using ECDSA signatures
- โ Blockchain-style: Hash chaining detects any tampering in the sequence
- โ Deterministic: Consistent hashing regardless of object property order
Crypto Utilities
The SDK also exports cryptographic utilities for advanced use cases:
import { computeLogHash, verifyECDSASignature } from '@untamper/sdk-node';
// Compute hash for any audit log
const hash = computeLogHash(auditLog);
// Verify ECDSA signature
const isValid = verifyECDSASignature(hash, signature, publicKey);Note: The computeLogHash function uses deterministic JSON stringification, ensuring consistent hashes regardless of object property order.
Health Check
client.logs.healthCheck()
Performs a health check on the ingestion API. This endpoint does not require authentication and can be used to verify that the unTamper API is available and responding.
const health = await client.logs.healthCheck();
console.log('API is healthy:', health.success);
console.log('Message:', health.message);
console.log('Version:', health.version);
console.log('Timestamp:', health.timestamp);Response:
{
success: true,
message: "unTamper Ingestion API is healthy",
version: "1.0.0",
timestamp: "2024-01-02T10:30:00Z"
}Error Handling
The SDK provides comprehensive error handling with custom error classes:
import {
UnTamperError,
AuthenticationError,
ValidationError,
NetworkError,
RateLimitError,
ServerError,
ConfigurationError,
} from '@untamper/sdk-node';
try {
await client.logs.ingestLog(request);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid request:', error.message, error.details);
} else if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
} else if (error instanceof RateLimitError) {
console.error('Rate limited:', error.message);
} else if (error instanceof ServerError) {
console.error('Server error:', error.message);
} else if (error instanceof UnTamperError) {
console.error('unTamper error:', error.message, error.code);
} else {
console.error('Unexpected error:', error);
}
}TypeScript Support
The SDK is built with TypeScript and provides full type safety:
import {
UnTamperClient,
LogIngestionRequest,
LogIngestionResponse,
Actor,
Target,
ActionResult,
AuditLog,
QueryLogsResponse,
VerifyLogResult,
} from '@untamper/sdk-node';
const request: LogIngestionRequest = {
action: 'user.login',
actor: {
id: 'user123',
type: 'user',
display_name: 'John Doe',
},
result: 'SUCCESS' as ActionResult,
};
// Type-safe responses
const response: LogIngestionResponse = await client.logs.ingestLog(request);
const logs: QueryLogsResponse = await client.logs.queryLogs();
const verification: VerifyLogResult = await client.verification.verifyLog(logs.logs[0]);Examples
Complete Workflow: Ingest โ Query โ Verify
import { UnTamperClient } from '@untamper/sdk-node';
const client = new UnTamperClient({
projectId: 'your-project-id',
apiKey: 'your-api-key',
});
async function completeAuditWorkflow() {
// 0. Health check
const health = await client.logs.healthCheck();
console.log('API is healthy:', health.message);
// 1. Initialize client (fetch public key for verification)
await client.initialize();
// 2. Ingest an audit log
const ingestResponse = await client.logs.ingestLog({
action: 'user.login',
actor: {
id: 'user_123',
type: 'user',
display_name: 'John Doe',
},
result: 'SUCCESS',
context: {
request_id: 'req_abc123',
session_id: 'sess_xyz789',
},
metadata: {
ip_address: '192.168.1.1',
user_agent: 'Mozilla/5.0...',
},
});
console.log('Log ingested:', ingestResponse.data?.ingestId);
// 3. Wait for processing to complete
await new Promise(resolve => setTimeout(resolve, 2000));
// 4. Query logs to find the ingested log
const queryResponse = await client.logs.queryLogs({
action: 'user.login',
actorId: 'user_123',
limit: 1,
});
const log = queryResponse.logs[0];
console.log('Found log:', log.id);
console.log('Sequence number:', log.sequenceNumber);
console.log('Hash:', log.hash);
console.log('Previous hash:', log.previousHash);
// 5. Verify the log's cryptographic integrity (client-side)
const verification = await client.verification.verifyLog(log);
console.log('Log is valid:', verification.valid);
console.log('Hash valid:', verification.hashValid);
console.log('Signature valid:', verification.signatureValid);
// 6. Verify multiple logs with chain validation
const multipleLogs = await client.logs.queryLogs({ limit: 10 });
const chainVerification = await client.verification.verifyLogs(multipleLogs.logs);
console.log('Chain verification:', chainVerification.valid);
console.log('Total logs checked:', chainVerification.totalLogs);
console.log('All valid:', chainVerification.validLogs === chainVerification.totalLogs);
console.log('Chain integrity:', chainVerification.valid);
}
completeAuditWorkflow().catch(console.error);Express.js Middleware
import express from 'express';
import { UnTamperClient } from '@untamper/sdk-node';
const client = new UnTamperClient({
projectId: 'your-project-id',
apiKey: 'your-api-key',
});
function auditLogMiddleware(req: express.Request, res: express.Response, next: express.NextFunction) {
const originalSend = res.send;
const startTime = Date.now();
res.send = function(body: any) {
const duration = Date.now() - startTime;
// Log the request asynchronously
client.logs.ingestLog({
action: `${req.method.toLowerCase()}.${req.path}`,
actor: {
id: (req as any).user?.id || 'anonymous',
type: (req as any).user ? 'user' : 'system',
},
result: res.statusCode < 400 ? 'SUCCESS' : 'FAILURE',
context: {
method: req.method,
url: req.url,
status_code: res.statusCode,
duration_ms: duration,
},
}).catch(console.error);
return originalSend.call(this, body);
};
next();
}
const app = express();
app.use(auditLogMiddleware);Batch Processing
// Process multiple events
const events = [
{ action: 'user.login', actor: { id: 'user1', type: 'user' } },
{ action: 'user.logout', actor: { id: 'user2', type: 'user' } },
{ action: 'document.create', actor: { id: 'user3', type: 'user' } },
];
const responses = await client.logs.ingestLogs(events);
console.log(`Processed ${responses.length} events`);
// Wait for all to complete
const statuses = await Promise.all(
responses.map(response =>
client.logs.waitForCompletion(response.ingestId!)
)
);Development
Prerequisites
- Node.js 16.0.0 or higher
- npm or yarn
Setup
git clone https://github.com/untamper/sdk-node.git
cd sdk-node
npm installBuilding
npm run buildTesting
npm test
npm run test:coverageLinting
npm run lint
npm run lint:fixRequirements
- Node.js: 16.0.0 or higher
- TypeScript: 4.0 or higher (for TypeScript projects)
License
MIT License - see LICENSE file for details.
Support
- ๐ง Email: support@untamper.com
- ๐ Documentation: https://docs.untamper.com
- ๐ Issues: GitHub Issues
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Made with โค๏ธ by the unTamper team