@anchora/sdk
v2.4.0
Published
Official JavaScript SDK for Anchora VaaS - Blockchain Data Anchoring Platform (with Hybrid API v2.0, Key Management)
Maintainers
Readme
Anchora SDK
Official JavaScript / TypeScript SDK for Anchora — anchor any data hash to the Polygon blockchain, verify integrity later, and detect tampering with zero data exposure.
Built for developers shipping audit-grade, tamper-evident records in healthcare, finance, supply chain, document verification, and any domain where data integrity matters more than storage.
Installation
npm install @anchora/sdkRequires Node.js 14+. Works in modern browsers and Node.js. TypeScript types are bundled — no
@types/*package needed.
Quick Start
const { AnchoraClient } = require('@anchora/sdk');
// Initialize client
const anchora = new AnchoraClient({
apiKey: 'your-api-key',
projectId: 'your-project-id'
});
// Anchor data to blockchain
const result = await anchora.anchor({
orderId: 'ORD-12345',
total: 999.99,
timestamp: Date.now()
});
console.log('Data anchored:', result.hash);Features
- ✅ Hash-only mode — anchor a SHA-256 hash without sending your data anywhere (the most private and recommended mode)
- ✅ Blockchain proof — every record is provably anchored on Polygon, batched via Merkle tree for ~$0.000007 per record
- ✅ Canonical hashing — deterministic SHA-256 over JSON (byte-identical to the server)
- ✅ AES-256-GCM encryption — built-in with PBKDF2 key derivation (100k iterations) when you need to store data
- ✅ File support — upload + hash + verify documents (PDF, images, DOCX, etc.) up to 50MB
- ✅ Batch operations — anchor up to 100 records in a single API call
- ✅ Tamper detection — verify on read; mismatches throw with a clear error
- ✅ Webhook callbacks — get notified when batches are anchored on-chain
- ✅ Auto-retry — exponential backoff on transient failures
- ✅ TypeScript types — full
.d.tsbundled - ✅ Audited cryptography — uses
@noble/curvesfor X25519 / Ed25519 (Signal-style key management)
Use cases
- Tamper-evident audit logs — anchor every record write; prove later that history wasn't rewritten
- Document authenticity — university certificates, medical reports, legal contracts (verify with a single hash)
- Supply chain integrity — anchor shipment milestones; prove provenance at delivery
- Regulatory compliance — immutable, third-party-verifiable record of what happened when
Configuration
const anchora = new AnchoraClient({
apiKey: 'dcp_live_...', // Required: Your API key
projectId: 'proj_...', // Optional: Project ID
baseURL: 'https://api.anchora.co.in', // Optional: Custom API URL (defaults to production)
timeout: 30000, // Optional: Request timeout (ms)
retries: 3, // Optional: Number of retries
retryDelay: 1000 // Optional: Retry delay (ms)
});Core Methods
anchor(data, options)
Anchor data to the blockchain. The Anchora API computes the hash automatically.
Traditional Mode (Default) - Stores data in Anchora:
const result = await anchora.anchor(
{
userId: 'user-123',
action: 'purchase',
amount: 50.00
},
{
metadata: { source: 'web-app' },
webhookUrl: 'https://yourapp.com/webhook', // Optional: Get notified when anchored
hashOnly: false // Default: stores data in Anchora
}
);
// Returns:
// {
// success: true,
// message: 'Hash queued for anchoring',
// hash: '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b',
// recordId: 'rec_...',
// status: 'QUEUED',
// hashOnly: false,
// estimatedAnchorTime: '30 seconds',
// queuedAt: '2025-11-28T10:30:00.000Z'
// }Hash-Only Mode - Maximum privacy, no data stored in Anchora:
const result = await anchora.anchor(
{
certificateId: 'CERT-2025-001',
studentName: 'John Doe',
course: 'Computer Science'
},
{
collection: 'certificates',
hashOnly: true // ← Only hash stored, not data
}
);
// Returns:
// {
// success: true,
// message: 'Hash queued for anchoring',
// hash: 'abc123...',
// recordId: 'rec_...',
// status: 'QUEUED',
// hashOnly: true,
// note: 'Hash-only mode: Data not stored, only hash anchored to blockchain',
// estimatedAnchorTime: '30 seconds',
// queuedAt: '2025-11-28T10:30:00.000Z'
// }
// YOU MUST store the data in YOUR database:
await db.collection('certificates').insertOne({
certificateId: 'CERT-2025-001',
studentName: 'John Doe',
course: 'Computer Science',
vaas: {
hash: result.hash,
recordId: result.recordId
}
});verify(data, options)
Verify data integrity against blockchain. Computes hash and checks for tampering.
// Basic verification (checks hash only)
const result = await anchora.verify({
userId: 'user-123',
action: 'purchase',
amount: 50.00
});
// Full verification (with Merkle proof)
const result = await anchora.verify(
{
userId: 'user-123',
action: 'purchase',
amount: 50.00
},
{
hash: 'e3b0c442...', // Expected hash
blockNumber: 29496609, // Block number
merkleProof: ['0x123...', '0xabc...'] // Merkle proof
}
);
// Returns (if verified):
// {
// success: true,
// verified: true,
// status: 'VERIFIED',
// message: 'Data integrity verified with Merkle proof',
// hash: '3a7bd3e2...',
// blockNumber: 29496609,
// merkleRoot: '0x9876...',
// verifiedAt: '2025-11-28T10:30:00.000Z'
// }
// Returns (if tampered):
// {
// success: false,
// verified: false,
// status: 'TAMPERED',
// message: 'Data has been modified. Hash mismatch.',
// providedHash: 'e3b0c442...',
// computedHash: 'd7a8fbb3...'
// }getProof(hash)
Get blockchain proof for a hash.
const proof = await anchora.getProof('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
// Returns:
// {
// success: true,
// hash: 'e3b0c442...',
// status: 'ANCHORED',
// proof: {
// blockNumber: 29496609,
// transactionHash: '0xb706858a...',
// merkleProof: ['0x1234...', '0xabcd...'],
// batchId: 30,
// anchoredAt: '2025-11-25T10:00:30Z'
// },
// metadata: {
// recordType: 'invoice'
// }
// }getBatch(batchId)
Get batch information.
const result = await anchora.getBatch(30);
// Returns:
// {
// success: true,
// batch: {
// batchId: 30,
// merkleRoot: '0x9876543210fedcba...',
// recordCount: 256,
// actualRecordCount: 256,
// blockNumber: 29496609,
// transactionHash: '0xb706858a2c9e4a8e3d5f9c1a2b3d4e5f...',
// gasUsed: 118330,
// cost: 0.0018,
// status: 'ANCHORED',
// anchoredAt: '2025-11-25T10:00:30Z',
// createdAt: '2025-11-25T10:00:00Z'
// }
// }anchorBatch(records, options)
Anchor multiple records at once (up to 100 records per batch). Efficient for bulk operations.
// Basic batch anchoring
const result = await anchora.anchorBatch([
{
data: { orderId: 'ORD-001', total: 100.00 },
collection: 'orders',
metadata: { source: 'web-app' }
},
{
data: { orderId: 'ORD-002', total: 250.00 },
collection: 'orders'
},
{
data: { orderId: 'ORD-003', total: 75.50 },
collection: 'orders'
}
], {
webhookUrl: 'https://yourapp.com/webhook/batch' // Optional: Single webhook for all
});
// Returns:
// {
// success: true,
// message: 'Batch anchoring completed',
// batchId: 'batch_1735131234567_abc123',
// totalRecords: 3,
// results: [
// {
// success: true,
// index: 0,
// recordId: 'rec_xyz1',
// hash: 'abc123...',
// status: 'QUEUED',
// collection: 'orders',
// encrypted: false
// },
// // ... more results
// ],
// summary: {
// successful: 3,
// failed: 0
// }
// }Batch with encryption:
const result = await anchora.anchorBatch([
{
data: { patientId: 'P-001', diagnosis: 'Diabetes' },
collection: 'medical',
encrypt: true,
encryptionKey: anchora.generateEncryptionKey(32)
},
{
data: { patientId: 'P-002', diagnosis: 'Hypertension' },
collection: 'medical',
encrypt: true,
encryptionKey: anchora.generateEncryptionKey(32)
}
]);Mixed batch (encrypted + plain + hash-only):
const result = await anchora.anchorBatch([
{
data: { invoiceId: 'INV-001', amount: 1000 },
collection: 'invoices'
},
{
data: { ssn: '123-45-6789', name: 'John Doe' },
collection: 'sensitive',
encrypt: true,
encryptionKey: anchora.generateEncryptionKey(32)
},
{
data: { productId: 'PROD-001', price: 99.99 },
collection: 'products',
hashOnly: true
}
]);Options:
records(Array, required) - Array of records to anchor (max 100)data(Object, required) - Data to anchorcollection(String, optional) - Collection namemetadata(Object, optional) - Additional metadataencrypt(Boolean, optional) - Enable encryptionencryptionKey(String, required if encrypt=true) - Encryption keyhashOnly(Boolean, optional) - Store only hashwebhookUrl(String, optional) - Per-record webhook URL
webhookUrl(String, optional) - Global webhook URL for all records
Benefits:
- ⚡ Faster than individual anchoring
- 💰 More cost-effective
- 📊 Individual success/failure tracking
- 🔄 Failed records don't block successful ones
- 📦 Single API call for bulk operations
getHealth()
Get system health status and monitor dependencies.
const health = await anchora.getHealth();
// Returns:
// {
// status: 'healthy', // or 'degraded', 'unhealthy'
// service: 'Anchora API',
// version: '1.0.0',
// timestamp: '2025-12-25T10:00:00Z',
// checks: {
// mongodb: {
// status: 'connected',
// state: 'connected',
// database: 'anchoradb'
// },
// redis: {
// status: 'connected',
// state: 'ready'
// },
// worker: {
// status: 'healthy'
// }
// },
// responseTime: '45ms'
// }Use cases:
- Health monitoring dashboards
- Uptime checks
- Dependency status tracking
- Performance monitoring
File Operations
Upload, hash, and verify files (documents, images, logs) directly through Anchora.
Supported File Formats
- Documents: PDF, DOCX, TXT, JSON
- Images: JPG, JPEG, PNG
- Logs: LOG files
- Max Size: 50MB per file
Hash File Locally
// Hash a file without uploading
const result = await anchora.hashFile('./certificate.pdf');
console.log('Hash:', result.hash);
console.log('File:', result.fileInfo.name);
console.log('Size:', result.fileInfo.sizeInMB, 'MB');
console.log('Type:', result.fileInfo.mimeType);
// Hash from Buffer
const fs = require('fs');
const buffer = fs.readFileSync('./certificate.pdf');
const bufferResult = await anchora.hashFile(buffer);Upload and Anchor File
// Upload file to Anchora and anchor to blockchain
const result = await anchora.anchorFile('./certificate.pdf', {
metadata: {
studentId: 'STU12345',
degree: 'Bachelor of Science',
issueDate: '2025-05-15'
},
collection: 'university_certificates'
});
console.log('Record ID:', result.recordId);
console.log('Hash:', result.hash);
console.log('Status:', result.status); // QUEUED
console.log('File Info:', result.fileInfo);Verify File
// Verify file hasn't been tampered with
const verification = await anchora.verifyFile(
'./certificate.pdf',
expectedHash
);
if (verification.hashMatches) {
console.log('✅ File is authentic');
if (verification.blockchainVerification?.verified) {
console.log('✅ Verified on blockchain');
}
} else {
console.log('❌ File has been tampered with');
}Get File Upload Info
// Get supported formats and limits
const info = await anchora.getFileUploadInfo();
console.log('Supported:', info.supportedFormats);
console.log('Max Size:', info.maxFileSizeInMB, 'MB');Real-World Use Case: University Certificates
// Step 1: University uploads certificate
const cert = await anchora.anchorFile('./student-cert.pdf', {
metadata: {
studentId: 'STU12345',
studentName: 'John Doe',
degree: 'Bachelor of Science',
graduationDate: '2025-05-15',
issuer: 'University of Technology'
},
collection: 'certificates'
});
// Step 2: Student shares certificate + hash with employer
// Step 3: Employer verifies certificate
const verify = await anchora.verifyFile('./student-cert.pdf', cert.hash);
if (verify.hashMatches && verify.blockchainVerification?.verified) {
console.log('✅ Certificate is authentic and verified on blockchain');
}Hash-Only Mode (Maximum Privacy)
Hash-only mode allows you to anchor data without storing it in Anchora - perfect for sensitive data and GDPR compliance.
When to Use Hash-Only Mode
✅ Use hash-only mode when:
- Handling sensitive/private data
- GDPR/compliance requirements
- You want full data ownership
- Cost optimization needed
- Building stateless architecture
✅ Use traditional mode when:
- Public data (certificates, documents)
- Convenience over privacy
- Anchora-managed verification
Hash-Only Example
// 1. Anchor with hash-only mode
const result = await anchora.anchor(
{
patientId: 'P-12345',
diagnosis: 'Confidential',
treatment: 'Confidential'
},
{
collection: 'medical-records',
hashOnly: true // ← Only hash stored in Anchora
}
);
// 2. YOU store the data in YOUR database
await db.collection('medical-records').insertOne({
patientId: 'P-12345',
diagnosis: 'Confidential',
treatment: 'Confidential',
vaas: {
hash: result.hash,
recordId: result.recordId
}
});
// 3. Later, verify with YOUR data
const record = await db.collection('medical-records').findOne({ patientId: 'P-12345' });
const verification = await anchora.verify(record, {
hash: record.vaas.hash
});
console.log('Data verified:', verification.verified);Benefits
- 🔒 Privacy: Data never leaves your infrastructure
- 👤 Ownership: You control all data
- ⚖️ Compliance: GDPR-compliant by design
- 💰 Cost: 60-93% storage reduction
- 🏗️ Architecture: Stateless service (like OpenAI API)
Webhook Management
getWebhooks(options)
Get webhook delivery history with filtering and pagination.
const webhooks = await anchora.getWebhooks({
status: 'FAILED', // Filter by status: PENDING, SENT, FAILED
startDate: '2025-12-01T00:00:00Z',
endDate: '2025-12-25T23:59:59Z',
limit: 50,
offset: 0,
collection: 'orders'
});
console.log('Total webhooks:', webhooks.pagination.total);
console.log('Summary:', webhooks.summary);
// { PENDING: 10, SENT: 130, FAILED: 10 }
webhooks.data.forEach(webhook => {
console.log(`${webhook.hash} - ${webhook.webhookStatus}`);
});retryWebhook(recordId)
Manually retry a failed webhook delivery (max 5 retries).
try {
const result = await anchora.retryWebhook('abc123');
console.log('Webhook sent:', result.webhookStatus); // SENT
console.log('Retry count:', result.retryCount); // 2
} catch (error) {
if (error.message.includes('Maximum retry attempts')) {
console.error('Max retries exceeded');
}
}Batch Management
getBatches(options)
List all batches with your project records.
const batches = await anchora.getBatches({
status: 'ANCHORED', // Filter by status: PENDING, ANCHORED, FAILED
startDate: '2025-12-01T00:00:00Z',
limit: 20,
offset: 0,
sortBy: 'createdAt',
sortOrder: 'desc' // asc or desc
});
console.log('Total batches:', batches.pagination.total);
console.log('Summary:', batches.summary);
// { PENDING: 2, ANCHORED: 20, FAILED: 3, totalRecords: 1250 }
batches.data.forEach(batch => {
console.log(`Batch ${batch.batchId}: ${batch.projectRecordCount} records`);
console.log('Sample records:', batch.sampleRecords.slice(0, 3));
});getBatchDetails(batchId)
Get detailed batch information with all your project records.
const details = await anchora.getBatchDetails(12345);
console.log('Batch:', details.batch);
// {
// batchId: 12345,
// merkleRoot: '0xabc...',
// recordCount: 50,
// blockNumber: 1234567,
// status: 'ANCHORED'
// }
console.log('Your records:', details.records.length);
console.log('Stats:', details.stats);
// {
// total: 10,
// byStatus: { ANCHORED: 10 },
// byCollection: { orders: 5, invoices: 5 },
// encrypted: 7,
// hashOnly: 2,
// withWebhooks: 8
// }Analytics
getAnalytics(options)
Get comprehensive usage analytics and metrics.
const analytics = await anchora.getAnalytics({
startDate: '2025-11-25T00:00:00Z',
endDate: '2025-12-25T23:59:59Z',
granularity: 'day' // hour, day, week, month
});
// Overview
console.log('Total records:', analytics.data.overview.totalRecords);
console.log('Total batches:', analytics.data.overview.totalBatches);
console.log('Success rate:', analytics.data.overview.successRate + '%');
// Records by status
console.log('Records:', analytics.data.records);
// {
// total: 5000,
// byStatus: { queued: 50, batching: 25, anchored: 4900, failed: 25 }
// }
// Webhook statistics
console.log('Webhooks:', analytics.data.webhooks);
// {
// total: 3000,
// byStatus: { pending: 100, sent: 2800, failed: 100 },
// successRate: 93.3,
// avgRetries: 1
// }
// Encryption statistics
console.log('Encryption rate:', analytics.data.encryption.encryptionRate + '%');
// Time series data
analytics.data.timeSeries.forEach(point => {
console.log(`${point.date}: ${point.records} records, ${point.anchored} anchored`);
});
// Top collections
analytics.data.collections.forEach(col => {
console.log(`${col.name}: ${col.count} records (${col.encryptionRate}% encrypted)`);
});
// API usage
console.log('API requests:', analytics.data.apiUsage.summary.totalRequests);
console.log('Avg response time:', analytics.data.apiUsage.summary.avgResponseTime + 'ms');getAnalyticsSummary()
Get quick analytics summary.
const summary = await anchora.getAnalyticsSummary();
console.log('Total records:', summary.totalRecords);
console.log('Total batches:', summary.totalBatches);
console.log('Recent activity:', summary.recentActivity.slice(0, 5));Utility Methods
computeHash(data)
Compute SHA-256 hash of data.
const hash = anchora.computeHash({ orderId: 'ORD-123' });
// Returns: '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b'verifyHash(data, expectedHash)
Verify data matches expected hash.
const isValid = anchora.verifyHash(
{ orderId: 'ORD-123' },
'3a7bd3e2...'
);
// Returns: true or falseEncryption Methods
Anchora SDK provides industrial-grade encryption using PBKDF2 + AES-256-GCM.
📖 Complete Encryption Guide - See detailed documentation
Quick Start: Server-Side Encryption
// Anchor with encryption (Anchora API encrypts server-side)
const result = await anchora.anchor(
{ ssn: '123-45-6789', name: 'John Doe' },
{
encrypt: true,
encryptionKey: 'super-secure-key-32-chars-minimum-here'
}
);
console.log('Encrypted:', result.encrypted); // true
console.log('Algorithm:', result.encryptionAlgorithm); // AES-256-GCM-PBKDF2encrypt(data, encryptionKey)
Encrypt data locally with PBKDF2 + AES-256-GCM.
const encrypted = await anchora.encrypt(
{ ssn: '123-45-6789' },
'super-secure-key-32-chars-minimum-here'
);
// Returns:
// {
// encrypted: 'a3f2b1c4...',
// iv: 'f28d7b7e...',
// authTag: 'c3622a33...',
// salt: '990fd768...',
// iterations: 100000,
// algorithm: 'AES-256-GCM-PBKDF2'
// }decrypt(encryptedData, encryptionKey, iv, authTag, salt, iterations)
Decrypt PBKDF2 encrypted data.
const decrypted = await anchora.decrypt(
encrypted.encrypted,
'super-secure-key-32-chars-minimum-here',
encrypted.iv,
encrypted.authTag,
encrypted.salt,
encrypted.iterations
);generateEncryptionKey(length)
Generate a cryptographically secure random key.
const key = anchora.generateEncryptionKey(32);
// Returns: 'aB3$xY9@kL2#...' (32 characters)validateEncryptionKey(encryptionKey)
Validate encryption key meets security requirements (32 char minimum).
try {
anchora.validateEncryptionKey('short'); // Throws error
} catch (error) {
console.error(error.message);
// 'Encryption key must be at least 32 characters for security'
}Advanced Methods
anchorWithEncryption(data, encryptionKey, options)
Anchor data with server-side encryption (convenience method).
const result = await anchora.anchorWithEncryption(
{ sensitiveData: 'secret' },
'super-secure-key-32-chars-minimum-here',
{ metadata: { type: 'sensitive' } }
);
// Equivalent to:
// await anchora.anchor(data, { encrypt: true, encryptionKey: key })
// Returns:
// {
// success: true,
// hash: '...',
// encrypted: true,
// encryptionAlgorithm: 'AES-256-GCM-PBKDF2',
// ...
// }verifyWithDecryption(encryptedData, encryptionKey, iv, authTag, salt, iterations, expectedHash)
Verify and decrypt data, checking for tampering.
const result = await anchora.verifyWithDecryption(
encrypted.encrypted,
'super-secure-key-32-chars-minimum-here',
encrypted.iv,
encrypted.authTag,
encrypted.salt,
encrypted.iterations,
expectedHash
);
// Returns:
// {
// verified: true,
// decryptedData: { ... },
// tampered: false,
// ...
// }Real-World Examples
E-commerce Order Verification
// When order is created
const order = {
orderId: 'ORD-12345',
userId: 'user-123',
total: 999.99,
items: [...]
};
// Save to your database
await db.orders.insertOne(order);
// Anchor to Anchora
const result = await anchora.anchor(order);
// Store proof in your database
await db.orders.updateOne(
{ orderId: 'ORD-12345' },
{
$set: {
anchora_hash: result.hash,
anchora_timestamp: result.timestamp
}
}
);
// Later: Verify order hasn't been tampered with
const order = await db.orders.findOne({ orderId: 'ORD-12345' });
const verification = await anchora.verify(order);
if (!verification.verified) {
console.error('Order has been tampered with!');
}Healthcare Records with Encryption
// Store encrypted medical record
const patientData = {
patientId: 'P-123',
diagnosis: 'Type 2 Diabetes',
medication: 'Metformin 500mg'
};
const encryptionKey = anchora.generateEncryptionKey();
// Encrypt and anchor
const result = await anchora.anchorWithEncryption(
patientData,
encryptionKey,
{ metadata: { type: 'medical_record' } }
);
// Store in database
await db.patients.insertOne({
patientId: 'P-123',
encryptedData: result.encryptedData,
anchora_hash: result.hash,
encryptionKey: encryptionKey // Store securely!
});
// Later: Retrieve and verify
const record = await db.patients.findOne({ patientId: 'P-123' });
const verification = await anchora.verifyWithDecryption(
record.encryptedData,
record.encryptionKey,
record.anchora_hash
);
if (verification.tampered) {
console.error('Medical record has been tampered with!');
} else {
console.log('Patient data:', verification.decryptedData);
}Financial Transaction Audit Trail
// Daily batch anchoring
const todayTransactions = await db.transactions.find({
date: { $gte: startOfDay, $lt: endOfDay }
});
const result = await anchora.anchor({
date: new Date().toISOString(),
count: todayTransactions.length,
totalAmount: todayTransactions.reduce((sum, t) => sum + t.amount, 0),
transactions: todayTransactions.map(t => t.transactionId)
});
// Store batch proof
await db.auditLog.insertOne({
date: new Date(),
type: 'daily_batch',
anchora_hash: result.hash,
anchora_proof: result.recordId
});Error Handling
const { AnchoraClient, ValidationError, AuthenticationError } = require('@anchora/sdk');
try {
const result = await anchora.anchor(data);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid data:', error.message);
} else if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message);
} else {
console.error('Unexpected error:', error);
}
}Error Types
ValidationError- Invalid input dataAuthenticationError- API key invalid or expiredNetworkError- Network connectivity issuesVerificationError- Data verification failedAnchoraError- General Anchora error
Best Practices
- Store Hashes, Not Data - Only store the hash and proof in your database
- Use Encryption - Encrypt sensitive data before storing
- Verify on Read - Always verify data integrity when reading critical records
- Batch Operations - Anchor multiple records together for efficiency
- Handle Errors - Implement proper error handling for all operations
- Secure Keys - Store encryption keys and API keys securely
Support & Links
- Website: https://www.anchora.co.in
- GitHub (this repo): https://github.com/anchora-labs/anchora-sdk-js
- npm: https://www.npmjs.com/package/@anchora/sdk
- Issues / bug reports: https://github.com/anchora-labs/anchora-sdk-js/issues
- Email: [email protected]
Contributing
PRs welcome — please open an issue first to discuss the change. Bug reports and security disclosures appreciated; for security issues, please email rather than filing a public issue.
License
MIT — see LICENSE for details.
